From 6294530fa3e4307a321139bdf6f9dc8c2ee6688a Mon Sep 17 00:00:00 2001 From: "Juan J. Romero" Date: Tue, 31 Aug 2021 15:45:47 +1000 Subject: [PATCH 01/33] Securing NGINX added to docs --- docs/docs/images/owasp_burp.png | Bin 0 -> 46982 bytes docs/docs/securing_nginx.md | 464 ++++++++++++++++++++++++++++++++ docs/mkdocs.yml | 1 + 3 files changed, 465 insertions(+) create mode 100644 docs/docs/images/owasp_burp.png create mode 100644 docs/docs/securing_nginx.md diff --git a/docs/docs/images/owasp_burp.png b/docs/docs/images/owasp_burp.png new file mode 100644 index 0000000000000000000000000000000000000000..7276d5294fe705af127b88e1331f7b55b7355da5 GIT binary patch literal 46982 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
G``aK-GGE%g>^o%_uk}@&$rR_j7T+_wXxH>OX z=HByNp?+pMBT}~>^Wj&#jG>wj*MX}Li5#CZoEP|86q#d%7 ztXUi7F0K{}J9z~=fl_3DhJu>55|zzmXuV;qRngGw8E#8RZ*-q#>zlfzq15nj=3ME2 zH@B{*y&aj8ObOk~UniPx;r#8(+{xb&k8hoK73~42068i`x12xrq}_-1zx!gxs+z|) z=D@x!g`pIL=vTP%65e5QMt+s z5uRFG&9RABRH$+WS~@-F|5(e*BSs}_13R_W-_*tnUt|WZIFhF#8rz@xe}*H+YJg2N z8%4ag(nIkEsqn`Aut6Ufd#X%b*BjXD2NBiUi+0r?5n`~srJuz9$$vty|G}2zwM_b? z{9Sf=(rwvvJ%p~h-_~N9%CFy7=BxzjQ(SRU($DjMT7XN)s7J$vmbG4J^nr&yi`po< z$vRwH0WK1fk2mcrTj=7eL)f<}kd29Jk&`9`L(b<6d>AhdvU-a2fgqfO-S#vIr0qKs zM4_b6tvdQKM!6f)oH??YS9!fCx$?|+{xkbg@jFJXTGroUZMKPO8v~(uB3uy@+Kg@< zPV)sp^8!=66n%rVTa8jUs_$0p+BCPLbQrZO!jYrQafaAm#v)-ER+8ryvHZwKZP;y- z3Ml^hBjcOR)9e3fN1Q=x<s5Tve>b1<$zRosxc~QG3NrHQSz%hf{n7w&e=)Ks{~s}dCC~!MvsyvsgMPpw0;aVp z4^6;i8a9p8aG;9=8coT|;RKR0zBg|~wygwxY(FjL9eP?~DBr#m(Qx7F60W>#o1Uw< zc>VJ4sa_VV8b3u67E4j=Q`HOo?RVS6f{9$FLKLIj1b;tw-&z^M|LxzQ4}kEv@CsoI zQ&{)C`}Q-yAn7?K-&IfI1%2;wI|83=Mc?(e+x%;%;MmY?Zn%o6ER5+D-Y)`zSHfeR%#TnX4 zi7?XC#DWIJs?kFhU#_CC5<3d9pc0sWK>FWqdwz?bg6c)P)>uQVYikDR+$+#8s-~bh z5}kt1D@$G^k>TPE@pj*n354@ zN0m4K^1HgK3K7LS^_q;~t7_q2AdAs4a6+u@$2&yaARk?o_)AG2S^u}w1u}jNhf^Aq zklfBMFOzgx?{#DJUi2#@eUUIw!YU^ie2}R$aF*tpjLL zPL8m8%$C){KSK1f2CHQ2#<`}R!V7v49okcLTlQx2rC$hGMTUtD{}%N2c=boo$Z~V@ zT+LNjOG8-1ckQpa@bGy9_n+Po%GvgsrfUo3n^ zywDt0BR(r>V#9TVi8{;`p;Ss$-EJ+&FJ=USeIAYO^w zqn9wIDV~t%s8>!@LP9G?kwQNNie6-GsgbnZFaNWwtEcjJ=Kl6k=invZdI!_O6A`b0 zo_PM6hzwp_9m|}nTg3YJ{U`{*&$W?fy-kDY3miTQWNgGJ8NV0a68)ms!v4D#IuH!z zBv+?{f2+H7`g@*SMbquGYLv$@6gEUAxWf>x!+*NT+-fq0g0J`2iW1`e$gCyT?MSq3 z9((S(_|w_X8C<_({b>r=3l4U=xtjz&UW?UmLyBGE2+~@PBlfB)0Xicm2 zkJ8wS##&uJusK3Q*~Qs7*xvkmWbtrzVNK$ofUgx#7(tc8gZFR#v;X2!-Dbje)8@bO z3<1xra<`^tv&ktEy}bdbUkwc6zf!qrbq|7aVWPA5YIXCY z+N~YYtJD~$RT^Xv6cTlgW5z3K8$YRfQ*$UM7k7N=C2*r>vx81A_D*mJK+>r@{}l-zJQKc7WgY z{CjXPkYZ?UkEFlE#-6XYV%lb6Vyf2`%K+|U4NK(y{+>XTs)5&ZgQR1`7X^~-Ny4m= zRlsNn6FSS-;_*R3RoYnpoRz;R;C5PLS9bZ)QFG|mBb2<&vS@CoaN(_MZLih)1%(}w zy@|GnZG8Wz#31qC#t95uq=H2bdkH!u+yOn)wd;(3Azo?_FRE=nL<(@_`O+I2?~V?k z{EyPliD325pLe#mUl9=z5fdMPH~9GYFJBs#LehZ9DLtLASs4uMi;Z;@j3_a0(YCgt zoRZoKLWVctb#T3%=8l(Ff)4v|i?z9K^@W}*DW}Ac!a-UMhxf1OJjeg_2!&6rmuaG4 zcw!h%XHa|0Pt>mLoi2-Nh2cuk<~OO8Hs74m!>$^~ z6?#sre@<=zbR!C2>g41(5VQuN5|AcTP#FFFpH2+~>Xgqf9ImT>L7-St2Ckj_#ISE3 z(wX93ci54^!7$m1hzp;jrn~#_P~Dp*90u?G-M+SL=g3hWMPY$-2*>p`pjS4JYQO%& zp0`{I=i1!$>C;Em!@Vu&I84)S=x4OED;k;#o>&zWHTH0Ii}megMfS0OC}d1b6(DK? z$T-N2y?F5g!7tltrHz)B7En+J2Z!OIAtJV~iiU|kKuEW{IUdpGMe@hzW!47fue_y+ z_p3dYWmHEp-Wm{yG>e~~??Q@wt@RdOtQZp63y~jYJukaX@I*Lzx1MeuqXgD`yu2y~ z;c-P1iQLSObg{{a$ccJSAljTs+gv`|lR2+fhfZs--n_=&-&)J#CQ3yhA;T8KUd^^~ zP~@Ck=WK#OLPgCb1i|xcLJ(pCKaAcWu=yMUtVDel^3CL$^s3Ijz@)^aym}lNJmMcW z`C6~|qa9*}2#NZYGZZ%BahHFOTfZ2#dqJST^w(wMm5`OznZYP%nS%#xEe{&0z<6qG zo`OPN=M?h0cdD{cEoc<0YuhV1qRx&(E@S$-2!m!GBG7xAU6eQ?3Yb`<+tzIK>+AX@ zE{JrDDYrD7PJ>Fo4Wg7k)}9_Zg5NkZl87O)&xcL{Yb-LERR}zHdeU60c4;(m3&(bX z=qec!a^VMfr#5;TzMNaM2rRlPWNM_GIihx#)KZDyTle-_MQfU379Z-h2gH#0cvYjW zex880IZ3!~?*-q&qCmw&f=}9tIE+*ZjzE)D(BBAKkzm>Qc?N1PI-+=Ey$P97ZBaTN z=>E2c4x-pEUVTXjTXH8kp1hToLuAHBmt3cXAl9C?mY22hrDITHmN8p7$z3^?ExR(? zp8eprR)HmQ7TEmj(iiI9Z~GlWWq3Wiy}EWfQegMGu_veYgiiLHpiM6WcJxdzL(aj9 z)O2hww%#gG2DwYcqUM_xptpP$XZ) zJtN&>KEetVJ# z2NIXYq-KTo?7Fmfh~`fZRY#}?(SCj(OJz9?5!G-Va%)nERN(B^MD3u^m|9b++h4OKNbH1SasV@Zm> zX7d5d`G5GFl>O^NMI}6G!`^&{L!_h|##~(L`0$AXS4^~fL(p%0@!D}{Ns8{K%8Ic2 zHt{GNjXkC3@Ia%LmkUkBIMqnsv}}RW2MbB-FncM$=2Zr|ok zBf2B~vsSiJ{qkrfGdg^fXzwhLjGdzpsVF~{Y@9lD>WExIfC9wc)-Nz|$-{moAxKVsF^5RH~+3S8V_t5ef zv3>!rMz7NsWQ#Eg*#2b)k=%kjFky>O4$VA|kBV>8Vc*UI{)Za%A>&Z7UroJ97<$Z) zuR-ibsiFewT2XM+qW6)Y?HDWFX07=Gv5|`I~sWXs>mmOZ>>jlBs{Ziu<7k(X-gYBb*jk zDFv%s^i{djVdFXCEbv&lfLtXjtSYmA?4RnPWvBPLy$nr;HxilTkI8L5 z`6YnV;*f#NGgaLp9H3QvlDhUH5_303TYdVenb`W1-}-8atAp;n{|oe$`(8n2C^|kf z86#sEkjDUxE&98MCsek=M-;FzeycHI_$k$Vz}zk%VYZWW*I4$pw5V_4hI?(qZo$r; z7=|b%Ej*PB>s}Al%JfXFmLR;(>lI*7P^v!D>PF|%kfK|u>4!vtVlDZv$FCe$YWV@q=Nk3GCsTkV2zr-s>R=L z?%!`DJk#lsfrz7W>3&)wPlfBYNXa*aT|7=T%7ct@LP*X&8+n?Sk}na}&|HWx`NPK* z*0N2;?u4avsSb58Me`jvI2RlOHxCv=lHa~*bN2k+`Nie9+TnG+$|_V^#AcIe-7T>G z(jrf?P7`_$($~+T0)728Ou_rHA3x6I#qC(s-{Xf7i1}Zaxj0~*(g`7P!61S_bBJtl zBW&(`&l4BM8{zoZQHfy{jrM4x-HO463zcr{BP2qL(rL57OQkkz->16X&cWUHr6Y&J zf6HH3X?q&(y;D<8zV$rb%icGjalt}%ohWVf%_N!5J;|6kv=moWoz4*t6;qnryKs0V zaeb9EY+OCZa8v>pKIuM*){89R*GY!I=K#ZsALbRPT53_%2+6=-WUSA!AGn=3p7dQ` zEkg48wv)|7vNuEd!}jq>KC|BFvfTNaM>vZf!43lL)kalQrXdTVp@r}xk@$A)UW;GI zjZN2j`mh)lRh&I;8p3Ke6mH`pdek6Lp(?|Chs(`Rt)JLelVsoRo-c#AUhGcgN@7Bu z(~O4KkT@AiZ9yRk7%M6wg%shzc^d3^jofjLSuGDVgxnpM^<%+DOk`>KeOHiD4;7n#G*p{Aba6#tElg%_bjO{pY7vm zui4XLrZuhxlpx>X)>9eLNh8st7~^FQ7BbZfSJW1wY5tX?Y+)rjpHX@`H#2RlPh4&!G|(aGy9F}v0X~7wo6(ud$fHWobl4~uXy{VI_$Gp<%_ltndg>nx>uL_ zJ_5^!@4W1y|Ads(r1^GlGg)y(+jBkAX{jjE|5!E@QT&4cr_*F@QqtEw)iQW7oRD*V z8hsIPQig=dXJ$f)cp+jcANQM6UZ zZM4a`I;4?nVHj&B`QVg90+5N?J#PE-TM3;@wo^L=;1z*pHYq77Hui1EU!Yb0Edetl zS&gh=#Ej3WkmxAv^>}=m&6>EZRe80q>vB9Pk_+J~aw(d!R-=9`FMaZv@M*wbjJO?+ z2r&b;S9o=;tZWA#OUcwf)^#e+Cl~QztjKbB1l5eQ@xOQL5mwHxTp`- zg{zZAMMM7ZS6&<;puw}YJ_f2Y{^!zwHz9w~d7;V4$wo#-Ajv+tV-KV}-9eZNGBQo} zJAaCla)I*No@eQ&e)prxiIrp$NDn-&``061x5#(&GV$BK8?%~J2q;(oRY(g9x(Kdygmn%jEn~$84U^%hPnb!kD8C# z78e(RW*%tK04etO_wVbIidgUJ?%uqY0C{w_foA`&Y3Y)u_vOFo^}9cQcIh(M`31r| z60m>w_uW%9iA&@XTJCdxwB+pPu!QyxjZ5JPug1$by$Z_0BWY{P7{N$c*nPck2eZAo zDG#Ke-QBR>6w)AXxZ?HnfW8uvnVBh*&w`HzQhgv>2ec?iXlUmb7yLGV{Ue_b1#|Ns zWiYEQI#_TwRsJ+7GgU^uWDt~TOg(0T5aajTNvio@4_0z&y+q4rm*Hl!A%Go&q^^9% zwo3-}b4Dw5ydUImfjZo@mIp{x`T1J`^9OQIzybjydiz(G3N|58XTaQ0Ob(yijs0O3P4y7bpBxy>tIqoe) z)Wq&R_IxNVgg@a8U4sNc3Zf$o1f)Je$p^#_5tAczIGpXZR{F&RE{eFF>8H>fDJ1<5 zdI-7D;#xU|-0Yvt8X)O@4l>QK*Wj1n-F~$M%}0w&O!8s#Xk3HzyVU1mF|&PD2oUZA z!cGuw474rbm1JlREHiRuAO7lM!3nRXCa$M|kVi7;dPqo!>JRnkjEu)=O)D2SH;@=z z+=_0jN1DDgVSfCyk*G|>MeKEJn@~Ops^_>e)?KHovw`rqfbc9C8s4s}+rTF1zR{O!@cLO1 z#7^HiRxW(@Bsan)Gl$)QjCBpugF~qK>`(onr1$okF?k4yUt=hObHdLz;02Pc zw~YV%Odz|FjZqa#7hmI!sMg+p4q#FEM`>1(2+24;(q|rN9MAdP(yap?a5Tkwd~kPU z`==K`8Xp{;uKQkE^BNRv9~vTZ=^}1|88+iJMp+Pp3la7}KzA z2gqfd5S!2z6HfGbXFQ=F@ERu|BO(oM3q0&|tN<6Wu(ARH5+_imOqHr}O1PIiU)g{< zi2#U(fi{{TSFSeuJ^Qc-^jHtBC>G(7?jEqn8 z@KR!m$B;ClxG1RzCI8gjvIp)+1fGyp2nkW)6y1^K0oLlgJ)?Ltt`Lw#iHM5&`T6Z! z>wwb9>m%|PY4j!TISQfjz1RhUiziegWDpVM`yYn1h)mm|tMb^k@*8?GHwu`6##?*( zF&b{X(0{9YaF|o~XE!F2@D>sD%V}mC@FYZ5kSDhY66(RW{f&D|rg{la0eVMzZ?OAs zZ*DGt_kAyOrt974@;dAZ=9h?M9(In(vK$!-V#k~8*7}L64Fj$sla-gsmI(#tsPAYw zBUaMxhgY)_-c8eFol`HsKf1bHrEElYEx9$)BK5+yeC%(Zm^#o(ILS{M_@Gnr!XmYc z?Mpoj4I?9v8QC~F9gXMq0=+-MBDx;1KE`LYKrT|25M@m9NnRrNEA!O<0B-~b7)~2F z;!zu7?OK-rBVw3VK>_#8+(b)f1ZabOC>!fyy>3*mY;(-QEl+!^?A2qkR#AK@>9@Nt zqh#D>-a)EQw!gA}<;dRrMhVV+i|DwStP|0{z&?4W!?YQB5?@~o_y^!pLy>LKH0tP%cBzB_BTIh7(u+eWq9 zZx}19BOo;sFIMIVRUxp1{?Nb=dxFEf43*gn+i6OGM4i(j5E|1*xlZG$4^&CW0$-dmobSlHqGI11wgO@ ziV(SYd3mX+3*Vzm?&}jocU~)WK%1Yg(vCJV-%>MuzS$a+ymcC3;rGawH><^T-M{D?ZV&=Q~-vZ((-k)Js_*+T#cq z&WA6wC4F(0@@#lRmhi``ewdDji?4-1g^MBE(;k-(`|u#LIU7Mfc)K@bI{>^GO6~15 zL-p?Z+Q&Bcfzx5h-(>=UKV-hfO;0z8Kr@2KvSwK1r7+gxG*=Nj zY|bY=ZP_}bHFjQRUaFanI!=ggUvN~zS5hz#jT{zqf9iz1%EHt6TKt)NmT|T=w*|<4ueX6P!t2CO2F6V2?Ef;rn{f83NTjaD<%*?x&mcX`z&9zn(V)UIztGkLj=&v zsY&?Lj9^%SNPd z!eMWkk%}tNHJ%ojK0W5XX0fpbnFQmB@ztz~B4)cya|*yhyc?RDSkAr~>IK07z5=p9 z+5pf35vw-p{+4dANH&l;|Jktzp92*aIDnNo1BmSO^uIc-Pr2h-3$NISX5=;mp<*=g zzFFhXwd`zUa<`n=jx^qBX=zzm1h6Sq%g94#_aI`@27=^JSOj1nd*|jF!AO9r1$#%w zXI+*OSpc}(=aL0gXw@MVtdxyow$(8tw(+u}5M=*)FeuAC>Nf#m}i8)%g)&aW@u8jC6knH2|g6V*5d-Cgo zw^Ta*htxT@%+Yqh(~(0{<)er8TW~eu2eLc0Uj6gn1E}Xq21>y)ntTacB|AOm0U3opt z;QtNVzTt`3L}*1V#=FJtfJN!JQ7O{QzED3lEovn=)e$;?KYR@yU{D8$8Z*EtYPuc{ zpzLmBWF%ZUE3kLEq#L?x&4n2xinZ9(MTi~Cgzdi=28zgitj#i$$ zp6+xFMUKweF3XY?)avjz>6Jmj{)!b1_Ok{Z{^cF1L2He>~$UCiupI`c8 z8tt|(cP2o9V)knm}l{V@i&t?M) z8VnZo4X8-~Bn`0h2`TuJUSzp=pM#&8jH~C^T6f*tI2erCt!oCu;DpYia1WH#l`y!+ z?eE{BAT!%p=svytW3HVS~owbbhhjn2^$ZmY_6^8vk|0T@gYJZMMzQcv96EH-aXTsMHsM1m|BiudTqc$ z1=SbkZ{rG)ZBT?KE9#rDo=0l%rzjM76Cxgasr-+i?T)E|Q_C=@ zeE>j=*Zvi~-U4maZMqj(ev0{Dh6N z-xza+%5)30W==EOPP8~E(lKlV^6R*zv`FblDzaFtFcQnVInUo@?7)Cu-P)2ce43b; zfU=D{FZ~u>^IxU5bRd6jRDymzmM*A42Rc`92nh+DUHz2=BkW6JB?CcD5sX$tLuuxwC9CD4i&`^C7+VcpT{p5uh12YYRR$#jVk!iS6 zR>9jR{Ks8%7<)cAQjS7+aFO+&&4|a~i49OBOw7#phJiF8;fi?$Ijcw%5TVBtO)YGY z(!&B;SrcPpQ0oSc&5eyDIZpP8J0QoVmQP_b9!dg5DRU0ZAWi@hyltSq++Gc*XD|2{ zs45Bz3p=%Kp2-0F4(I>T^xg4T_J8}QnN7OLCS8@4q{zx9R6+<53W-QkA-fPV%g&~Z z2vJc+$<7{0NyC;V6@}k%-QVZe^T%_)zV{v1b)KKkdmP8{KHkmzzQ=!mevMJOk*x<} zh(A;5=UQR})02}g?|Jp;{)TeU8vmP16;#hs^-ufl)ypT`Yj%3C(4)uX8*f@d9ToL-3u_yd?GIA^GT_^_b-Sqm`t=&1eC(?5wM>-S?M1d-~` z!lQ9&9*Cn6un%p&X;O4A@d9inzCQigjZBCEP={ALaY25z@Sn4O>Ro~HhQOPp`S}h2 za`ENpI5us1;Jf5vmCt`Ni<)PT(~T4N13|tJc*vOB(+v`y{eAcm1Jv(%ZqOIR&HrON|TpFe(h zg3+Hm4H6Jg56g_^lluDl-xn8I*x2s821qeW z90xRk!Ba%4moEzfl_I@Pkg?0nO2%>nYMweT#abfXWd~(lIRJ#9dcm# zIB}F7@;oeHGahnFo^SW>cxfTa= z1Oh_^8@60?d4hbvZ+^_nt4eS`@!+-JZ2vY}+i^T!4G!nO*Dv0@zhO(hxVgO3-n{~? zmQqQzb>1yI^UJQS@{=Q!esgTxsIdnE7Hb#R!tXZhgHk*s>u zKq!uS01m!ge{lJoy|J|ojoO~$7QP4aix{qt$ox9d%hO!ubLz9j^5C(GW}%3>`o57X zQQIWUwRPw|XERvLm?<5!v3S_s?HBk~;kA?rH5rfT z>L{OJyg``g=;*)=+kQBWOk(PLIr{FM(9kgEkTC>NJFj)$2k$*RyfuB_IZsbdZCLj``^sU zip}438TGe{w+AMs9-prs?=5a7?9QC2@&9{?D1Y=QxpNXS>cS<<217AAvCd%AkVd91 z^O3&z2s@sZH4j@})H|qhh;QI42O=baA=UdSRyyrP$s;dMAGGh>yOGzg`+VT7F?&Y9 zS3=^73W9B%;o#f1Pv>6KNYp#`iC(zy69h?XYF`{0?Z!%Tv{q{y|TXM@Ajo_LzO(u($Am#u;eHxn8f%mD2GMyX(yF@+I&T$ ze{AG4Mt&P_^{BJ-Dl*ZUzch~x{8_FhoTqW}Jn`6bl4-@x%`G~K?%A_vxFDEP%5=^1 zs(Rkm(ZMI{paA2h2E;XY6eJ2`66gm|O`jhak~@w09gG*Nov+NT=5@v~wU*w@6pE0)XfmH3Os&bJ zvpen*qvrni5vnQ|LaxQfP15wA&Zd4(Wyf+k9>LbaC~)rU*Ff+A-{F_QyN8}Y7NoKR zDnxK_a1ew5+YU_IBTw+rO?oO447Ah&ty%l8gemz(=0crMB_bp94Yw7pA&2zzjMR!u zif9ReR)}GU_xBste*FHu?%Xt}R&qIW<|pFxq^A$2a>H48sfo6jO=nWhVXg^7VKvs$ zH`|T=PI@k*K`JS%3DewDdPNd*je>%L?Ck6q;&1tTwS?P*sZynlIZP_>`oCQ&7q1@h z<#^3xvPM%Wl1=;}1PFomS@tQ3LgnRtLcO2=B7`H#FtM}SqE*32Kjzg5JB&xet^~ne z+0#+pb3%{&Q)7>_X>VWOdBkjXA(is#>Yd`^8iKoZjIE`AW4N7=acJWZXzhEf`MZ2- zYHFql<7nw}uJWLF_Kp&q*l>IwjZCUcavecX;45N9F#;Jb*t8qBe7o`>kng67{aBJjPppe zzdL!t%w8Vh&^~{DcWmn(d3jwMvg~K$o*S|_kRrSr$2t>}l1xaBXV2c~4(XTskv-a* zVq9Ub5Oa#>&YEQ&eU0@AE31wV9=#_+boa*^6i7%(?cKL;+CxDik*Qi>C_>eBZxK_O zbxm&b))PiXa1zqDv9VFjI$~_9sreisIOV?dQK~Ie#++i!Mglb3j%fsJW+47)#onLf z3IALYR4sg3c1Sww;3S<+zX0)4`&EZ_E7P`O(_+{~;k`&(R%#cRL<_5(Aagdiw4|k{ z18lfSUS3&pRn3a^R8my5w6si2PTu<9{PE+*6UY$e@$GYeTXp6r;r;tdhK1C_M~{Rd z-NQ|Sb3>2rhq5G>0}Rs1Q4@-LdBV;tCRW|#kM`QSH81jXU)#}&hmksa`HC`a4;(m< z=fm~WLqPy$RndzQX{K^kEVweQ=mJGJVDr^WBeHdiW42gGuAaz?xC$PUr8>2p%eoqd z?K(eDZ@wRTHhQu%XS61;SZq#r#g1h$pj>&p&zHeK`(_SIuu=~V=j(3*A$Ia4CyBHd z^UWwy_RGuvdi~(E^QLVvpW@@=VXJ}%fI{u}*YQ_4$mh>9va-4j-+K*DjVu)(PCmX` zBspYj?wwXCsi|AUshAIrzDp}S7SfT)vmIi;mcp_&CIIb->V z$AleRiJYC`WAKFHFLqn;!s^NrN|@o{;my+4&WN|@+tqr8D*fu5OKDYMv4{H^edESM zuq((SE3T=RjF{pWH45S$V`RS?OKY3)=QD68{S0XXD{&Mzf-S2wmAA>{78Ww39-*QbglK7L z#V4BC@F z*CbP89aB|3%B|h~oM>!rrXxHAE?>HIXAg!xX;o)@b_463J)pHii(gewA0Lmbc1@0{)^z9TCF~XShkK)Zew@!h;PCUnp5(KECVQ zMKAT~PG_crZp&|OMTJhf9+;;R5fQON`a!1vKQLuDbof{4-wg2(I}-Zzf>FT2IabNB z*}ywz0?f(^L|6Bq?dmt0t25Cj!gD zJ`4|PNXurTQiQb=;f;x;_^k(uFN?jXpklkAB2``=l)`@VbGoI4Dplz*0)hH447}vz zGSmX=&EJW@5{8DFnx2IxE-?`n04bo?a*Y0<5ILZ$TYM!+T2gXTg!Ur8o+oWI|)zHa#-6v%jzdAG7kw~lIQDcn~DedH%}l7&dd}7ahd+i zj4f}lc>H$M=ldMPwAUy3HKMqPFne@pyu7?8D}d1+&oLUbHtm@76ci9hlCs1l!BCr; z)9KU7ii)XeY4A2w+DCS@U!KzlE5=yyr1ye^#(Cpn@2HZl#^+vD7F2dTm;U~8+pr-V zu)v$qQ4IaL{&f&}9g~{MJ35G?{SF3xr7m{VpWpn5*5r5rgS&^gQ5U8SS)Vx_Hj+q; z6;>EItPUS9^PV zIoGvUMC1e$LDnC(e)`P0DqZ=h_jk>@f%~yV!3G5@$D}Tnv1R4*4BnMs+*U*$$!L6u zQ(s_WX=C(fFF_K^6>l6rXpumEboRB0scDtVP%+N@p|ri?P;bl1RvPE&q6)$UW>0&& zu>`Jg5~yZSs^~?CZ;P`>6UcuTXD@txw;%rnkT#f0#gf~>2t!`rg%a4T%%n*E>_}Wj zM)W0yCyyVuO?oPw$hMGTV`kpOK#O|0K<)x^HG(Q?rv&wUsh^jBPSa{E z&J8h#Djzv`5C;Ubl<&9q!QtTttgNgI3?gf4e1I_~N!vuKuw&c}pAD$@>yZ%!F|oFm zmhF;~C7^}HPA(*C;g(rj3s?#9^6+?l9x*Nv9D{EV5}z|>XAuj3A$}ktzp_2*ic{W# znqDlDPsvMcflpf^@x)Vad~rjJM4M;p>gYsh=c@XznA+rAa*<{Wzgl7(#azfO(CRbi z!Q*tZ^@E4EwRCJ~g-6X>K8t+0%CLvt0`3|CoA zRaI!I8!=cj#{)MbI5-$cPIOdMXIoo}tRquGXzi0HM@fz@F2zP-s5meG`*VJ&;|SdB z(03!fK_hRDcv8jZXRuiV?-#j{ZZmia9B!z2>%Tg^Rd-j0qcZP|jt zTx0u99Q*S%Z}Fb~EH8_bA~EN$LFQCcR-T@i$bNqvo?v3q(yCrFz3}FeVc8=mhodJb zV~1|yCFrZUi05;QXV8bix?&=z=I&(87#t=<$R}_Y6^Vo4T1y%jLSF0+1y=ZAs zQcyq&wzRUsKlGM7LP4CTPcK-ZUSx{?a}Dx|l81(+1>s9>g2DDY4}AStJy++s_Nw*8BND zN|N8YSO+J#f4JG+~+lCn3JE(4sWU44iNufXgjNwvDThIi- zx&SSavVsBt!Wk49a%YN%SK;;a72qDix$AkVar%2y?T>;a8!dw_5T!F@vZ|1bT_XH^m@d!}8lv5GKg7MUVNiB=TX z*RSEZ!A>mzumq_L2LQqBo<#-qLKnPcBO)SDj~&$4H!d>8NH!@e@sN6oxEz3tVgc(|0}=02(9N|kUr=Vd|WC! zaGaQEsdJ3H@s2gF(hucZm5ynkoEd+7{#0MC4&i~%NHLE|FDc}**V6Ef;;l6s7F;~0 zvG7)d2Hu21OtbLJJ&m5&-U9VaR?ogYdv5BHkxyQ{AY-%w@fDNtDndtWZ84#rk}F1{ zdHCPtPd@$)np(!0Ss$_Df#=VuBksO9xm!OWEG%qBo1lQ63Hb#D+v ziNyY7r!!|P;Spm@+IgYyoXZTG5?qb&}1+Gi(|9HZ6jn*Odw$4{5ap9hc6DG7N9`p ztJW3U(WP9vbg3%Ks>0%|vvUgKWWN4QAYTAc5jE;zE&AvCCxq+AFy=&?w?gyN*yR(O z)qw*Wb{@nHcmkyt8q@*bVkHe!x_YfMlFc8!doX>HZFw9fl1SF5R1W4n%zCR)c*9b8 z_S8$>rEVe{CIffx9+-wFipH!LtrgMx>eXFG1+TZSA`@u{!7T}8w*H0c*=uP+Ei)gz z8qe1<_~!NT&R!}$4G<=~=lb08jf6OfMj0tpRSK5n?7xYFBj__r{9_eFC1qv)n!f?x zKho(ZfS1F^DfLe&5Ye)?K99`57NQ`S$HY94{c$1>6Mp z#8b%Yc6Q~E#=;x}X3FN@Po3FM+*UYG-6XR&2?^^V_msO?-bS<3=TtGL;$Fn@oR|%= zov?FKh`T3p{aN{yAYLXNV{j`P0TS>uqzq$mMi_Aj9X!|xi(|6LbD7Vm!^rwA&CPvV zcN3V3GJ8w&C-jBd0UF$DMHiX5mDv`cOaj|}A)n_KBHU)876CpE_v5E{+UQ&*N*_+` zjB9s*Nv-hilh@FaSxX-=G<1LWbos!kkw7M4pWh}_UtTTf&uz30rJ?^ur?OkBdg`F} z2TPZ5QwbemI7`Dy0zO^!0js`9j{pAoS@9qG3E35n^_H4Ac+W9~+TNpMvWf;c2{Q~D zNdM>yS&8fT3@W~h3`FIF2l4&&i0-(tCAn5RosCc%E?XJ4=G%nyR~wNq7ekD zQ|G%xO@RJ~?;3&WBv*tCt|6D*#K@aBd4+eKvzigY8%FwmTzLL&6L(V2wX(7=08eJ` zqm}`-^z{5EdIDGxO_R*lty={JGZ3d?v`X-(cz;76?dt0CQOSe>I?{u2vJ1?gU0tIj zW(SLI`d%AF-A)bwx(D=<)#u_S;Q#V&X$Y~H(E!T}diP#H70M&H;Av!mqfSam89|xw z`0-=J9W&#*!{+J~W41~M#_*<8d=!d#%5;=t0(odFt&z3#tAS0-425qV3<4T=SV!+*j;4FN1|V%lNt@Nw`f+4w`BXOIu}aSR32%#%jIPI zoiTydH*emww9t#ENv0JIF)mFz**6l4Tk&~7E=%E0V6R`!~ z;ND&N-TcFeDpB%<^E`5xrO*?#iuzVEw-k7iKIuMB>GUX`j@Stk;NeRj{46c%c^N`@ z6g?e~p1Y#NNKC=%>VJpYySg%D9V3xjQK{qKWAqIslDHoL!c?uEf?>qLF?$+@lSmz$ zji*`3g41hu+HSeF&&s5)B>N!T%PcE@GmBXtMuLdrHlw-rywFoP>Fq*g zjpp=U;G54k|GyRh=0ZDx0E;3o9j=5hsn{(hy zA;XGfa_7S5&rrg{-Y|O5zGA%NN(crVz)&qI-8jh4WTXN83d{_9hxKH)m)=;{v7 z{xxn;3HX=tmr9_uCrj;!u#(>&nPaSjl*!q)rlwCntB)z827+1_VrGngQj9~2T7Zb+ zZpy=w5GJ$(skkz!w{Kg!Wmw%OV85hv`O80aE|hW>cAul%_v+RcW6iu5tC+V{XUC}xvPa}rGON_LZ?IKYzMUwzc~Y`1|6FZEGZG~ zSDnXTuoT3;i+b*3jkUXm1{DFu?6%THgE4Uq|9(8h^9llJe;ka^s}VugCNxBB>!+W~ z5{U+A^#J-sB?PdqlP`ZQ9cyY*FaXt zxs8%avm9h!L&X4BG;T0_(>wG{#7F`4zg$`|_zXF%(e3V{qj$f6heRYbDEI+~GGq$s zV4|rUAO{~WFMhD$_M1a54yF&&G(tHE{fIaR1L}@BWqD$&{h)7XLDC3ocuN66{84*& z+4&_5nJG~hI2RJ$Q@`aO6xxiAj+dW5k4K8)+XhAqmoB+e=SsY~TlB%de!8|%QBi$w z04z+-%_#vRd;C}d?s2hqPL#uADaQ!CAF6XO{#{_VV5bJ?Tcev@FX1yDIHCp&&<`8K zKc4%?C(T`6dI>6^ieRb+t8I2+)k6(~C=N~~WwJNP0EczY29}3H_C%ep&bYK{)_r^F zB6lt;IJqV#|8Ax)GLuNeYr^EUMngBwBtq9zPu8w@J%UhV1X=25Y>d2Y41&j1sT?86 zc(fdyHsz^6ZSmszMk}s+`!h31#HqCgp8@qpp#|^a^vDJUmE3g5huWgR=XPswkaW=X zi*6N;%*FZCBWoI;K6vmTV~Dy?BL088cUrF4#ObWE(x*?Yi%jwQD=co9pv062716K6 zE!@AuzY6~ks35!78-@^dGr-XUvx^%lETrP~2Gh52N4Z`Wo)f^|a<>_x-LQeeX|6F5 z$-8WIv%mrYEzH1mKsR*z_Kh<1kB`w>iqSB6n6tax=Z`BOR!?o&bsJxeq=SBTaL|;r z9F$uKS39Ke`t282mY2`eQK#&`@(>rxNNjMJg`&a&VGHIKH5LdxtH&N-#n?}sJn7VZ z+p;U^pl(DE&#+Zetns{?Mpci)wOalY(#Z#}^ci+%(so{}QRfWYI}pyUR;5R-EH4LXX7%uFs)dx{;ln#6 zB`G5#;*sd-hVg+>6YocVE>S$>ODNz3q1Hg3hj`J0Ki%EjOra=69$)?{6lu75U>PJ9n<`5DF@D-O*JPX)0QYk$hze$f1U~u00 z_f6TY3@@d2=7hw=gj%*a$eK4a8-4!#8ONKjTVGq{30p$b2Ml?J;{j=uFS|L*NDL0d z1VLVTB|}d!Q?IBvgE|ybH+Kv(BsdtRx-f;4j;`z{q@V4}Ku2}p+PV+->l zQ?%9(D=QIbaMExGaZiug*=1|yI0I~z3r#IBcf8eqV=~X^rSF8rpiwO2KLiUzpslv& zhPXinQmC`B`OVjZgP*~YO<2YI{p2Jafgrcm6S$TqowrrL64I(FLmszp0M8N^M75pw z5%^Z<{*dNN(p6s3vW0(wTmT^9Q=bb54zxlH0-pNcwA#A3w6xQ!QM7UwLrjYNeonEj zaw)d{{{356{%i{Lg&gwE6M{0r4V!9s#;>to};TXh@1Ga5h}-^ zh5H7qjumg~UloTF~qIwU9N&u6 z3)M}O{UuRRM~}ehOoM(8-uOK+YLlMvl4JMrP2~?MyN1I-L67Xa_D`{r+T;;tO>>d@ zs_I|0x6k5OBH3S$jXm(aTS6ai@@4r9a5i-|) zH>?l&Tul**`Sf-80S$4p=^QZ5SHUQ}Dw0z)X>=N_aVl6mBF`xvX`03NrP!6oY zYLkyiiGX})AG|n!|2}X+Ub2J?wMI|R{mMLma_Gyea+r0D0cWAk?0}N$Mo7@B5Tqvf zNeBGTlUR~SNl)L*#f9EN0v6PjqNp$_%VN|5*Dz~pgv}&pZYw|Jqe>}tPk=V zd1h*9nMAJz>AkP-3eE&py(D)Y*4HnhRK=Hcf9~YqW$ekMBD@xu1eLP)M&=@AqSm2U zPXpY=Q3YX!>AsQCQD4-2$SCu8@*r=BARxBW(|?zj5r5Gn^Z|ZFhb%2!h7lp)i*NC2 zaM93y{6VP#s1nB;c$I^rqfMPJ*X-(yC50fzrSZTbltcqFASSq}z;~^!RWR@a(NC1N zD9G@A`ySz+@vQ-r{hk||N7IEwh2K%NymU+aDBR>#z!QQzjLZe&hxGaoE!w6{H__|) z`ud)z^#Y!K9|;BNLq|tQ#hU|WX&W~J;=-U52J(P#-7_gdwD84V5q^ID)I%`rcX4yG zv9j`ASv+&)%3q9lQCw8VYT=>@IkS);3O5vKON)z8xo!ue3bHJ*suk~Q9R zBzj$PtLVgfJuP=ju!Ef0{yWF+udPPeK32b*^VkOU*+nletmM#s_ZaJ0mO;+g0`mv* zhuTFq>3Z9-89-7#?;C^B9D{`?2;_2O<37NU=(@!tHK!t+`GmN+o$T$;;}bvtwdkI= z$1qiPn}|q|dm{=%kb!y$6R^-2zHf_wdxZk%2okjP^xNS%93D>7h$rx_A@E3YEjrXW zCLljyDvrRk_QOFPOKWlqQD(66%1*%b`rEUgEdP;rg>Aqbq9P;lxlulvSy@FYWgv_g zkuIX-M3Fl&HASrsb{5PCTsyc>m7o8pooEnxd$e=%2UGmo9nuqgE9D zfQZ-#h$z&)Ak7sApo7iWe}y|5p|?Esf0!|V}0@bw;5R8SGf$(V~nY(}MgT#9=f z4**3z{ytis!UR+slB7tKSiffaPN59^wncj1k2I(;L7f$t%p$kW?$p6~rqy5w)dVyH zc7UomDQDleflQkj#UdbZ(7a+3kf%qf>ak{-d#Fh8M@5L;?PTS{hnFs1EY0A|ef;ij9mY; zt`6)M0N8gRg#Xho0JIe%ktVAm_UbQiyEg{lbDj z+M-DJCdnVoKCkU-IsbeOPdB69lRDSMdf)~@GI*q^Hu>?RCn^^FW{Jd({{c&Fy_kuK z;wBm=J&j)-NtnRdFw9gzZ40bi4*t*h#8}-04Fo%TETuxl_v_Es2hYOF&Yt|Ah@}B1 z3JVCJTf&z)I@eUV2b~4>B6@yVkMZ z$|xSEK1=b!9k5&2gfxrm0(1iV3n=j#st4h)eFCj#db}_qidhAG-n(LRxGH$YSimHu zbnOdc+P~Vzk70{V*>i-HJ%gxU8mNHd6np29%aDYuEGHdxo)}Z~?wMU(Wt&+79nsG`=uwd7foh zaPPq;6W7bVdd2F-BpY-dx!MWjciq9?d{qwC}4(0F{orn}+s3naFEp-g>AIS=Fr&$3tM^Civmdn^Qbf?d0@dM%(lp6#pE1IQ& zAtTyri_;*w_)?91k@C>_!=ecX7PNKWma5VvY-`Z4g|3Eyz$GpHWu4WAI84}KgNs)O zjt3QGOVEs~v$NP-E`l#}+GJ;Po>tq^n`?}l#3HGQiAj zJ9m_nmGgSk*8Y6L7nhuhZ+9R>4s5qI}f1mI5=^bA<9j%dis5$v&!6hC(K9_ATi}XoNR;S$D{3qHrvNY)>+$R%G|d{%9nyDkkytTEhdHfmBgPH1 zY&UJ%M3s$C&NmJ1V;k0+c&ofaK)V_ef|0b#INrFXL#UwM|6ImxAt)6P>3o&Dy|0nC zP*%?;&zw1)(^9tbdJ?NO$Df%TBr(x3Wgapn=_eqKcAhT0xP;B@T*i*RG$ss$2MLPkdn$`bkImoOB4zXOOu^|J z$FV?7(YCilwkop_7(pf)eKZ|b%ONOj?2FFfD_+K|)kQiv4Gw%bB=Ne7^W3AM*~Sjf zuQB42F!m6vWX4LLfF~Z z%q0?mGGTqkUEd7wlpwV7<)(q7*I@qyI&-MjNAd3^`0_%Pov&F)QD{kR(LU6QZf>-lgD6U$Qr1 z@Ci-Nz1U%qxQria<8;(c;+SFDR%BZFok(F0v%klgGRglTD*cm;4dcuIP(P+i15zv> zA4mq{ilYd{EJkrxYJ5B%iN;qdC;L98(qz-BL=#U>Sy%Dz8%M7EU=``?+8MA^{AWn= zjA4xL3JP|B|6@SfuxW;91VBRJi9xGWUA%yrtF7>=ZjO;1Pf|uksc|5bt?`;)^Yq9G zxO90vD3hF%IhYUb?DeJd01SZr2mr)z3sCajW=+4#m^UmbRlRk6b|9tV)NUgA!gSi& zzdZ(~VkAJEfj_6vjJjbl2?|Wq)6u`)eo}actvgV6MenTE+tEJ#6Cs6y2LTvEM}Rv;^GBxEnbLn>yT) zXYjHhM>$^QywOv=>-i7kDtA)_xd^Bj z@>)=}apM|%23;TI=o-t}5_3~tIVA-iUFhfj`)%fw%Y7oN2Qxx4l~brn1JK)y9*wjg z!F9o3mAGOn!vdVG1C#=&84M-p7~_BoAdKSkGKPHn_Raat!v$Q)mET{nZME?J{auL$ zbx)rHk_`S2L_(GRIAC=Jg#kvd?xNO*Xa+lrfPVGht^fZ0N=9=%5)=>^3;{OEzh7Bd z5d+~FFTwzMsETNtLnSB|SPX)QL(hW}Yc%2vG*`id_tAjNS5;K3 zdHVD%3caDBL~fd?$w@~@Jscfp=jR_e_l5l8p&~wf_<$He8H4!Lu!;6Omh7O?JT}i^ z`h<$IqXKV5<4bWhr*Z=Mb={TAXo-<0Ivgn8;#iStKu!RoudA(Hc+PZFRP)^V5HXuE zT~EcU=Df|Jp|Uw~sN8Tvz|swIyVuIgM-P|L5q0eMv3P~72x<}=q_8v930xrh#4H*6 z=O`j^j*u)M1q;=*cRa_cV~iy3b^68h&)qCH+a21@Jj2zF-}VH;k(d~^(a}8j@SX8` zEUc`?lg^8%WFb*oxpS=wkpl%2=HVN`CxVP&B0vGX$qY9-9~QTV zxt6#mFz(_joE08HA_S71o0*WA>3;5|gAyX>obWzy39Yf$4bg1KhTS~Mpr96%Dad@xspyfFu<{9OUIA`; z{QbQ#jBWLws;cYS`j~R*mmQs(ubWDg*6cG{*$IUiD#F(&uJI+HWY;wwM6Sn_47LS5 zM);1*#XXvxoLoY`)H6OV%%!p0fr*(J?7lxn2cUR`<{>f{CsCM}H#H+;VREt>RW8~* zW70(+DF6e$j#Oxg#*3Wp!vrXLV?8}RXk=4xvL8MAjVeeylA5yd6g?=)h2pZWUDG*X z@6=~&PJwaNF-Arsg1!jp7-N)`ct1bSkETFS&>zhnKrdmiIIlzwLWaWcKywrH$YR>h@aDxLDL^DH+Q*L{A@7;d`;6uXAB^A-5IAIP zikyk2@1QX$Qu|g~n$-gD>c4+gLWG~cKV_K-3Kb+MS2GXbErF{`$wp#t@ypOsl@rI3+I5i1%J-zF?8-2%jEo`+CB+(t*|HEp(#(_xP3dQ)kM$FO~uD;oT zZb7jFqPZ55j_~d6W|Pc~LPj!^PPEb)uSSHS`Ml;b0Y+ zOXj4Q#4K%lO%+sNUvyC=NNJ0#tSofY7cy2m^QUMC9pkFshl#eAFR{h6<^u9BM$Ufz zTfX4S0a!yj91~|(&~;ySng4@NBSe74F%M5Z$_tGe(FK8biSP7toFZSCP9Z~&CG@5KIr6)+w zcnGw?LvUqj#_uVUCMb7dB?l}EUQ$Vb^H8x_A$C7l;BonqCTl+;s-Un$fWyXUtq*=* z$DV`_zS6s>hSfLNy|5&zmr6yMnB2tMmFR;>_m$g+3uOx(XfnaAM}CZO2qC(hScrygqg;DIp! zSrjg?g=1V0|E~rDB+EfSp%w3|bro4C?AkT!dOzN#a*D7nb3cFUcO=77i;9nVteC8c z9G0Z4Gs=BVF}n%kBJ_8=(KSXw%lm}-Pk-Su6J_R+c>n2>*55Vl&?8^o$lA z-9a!gF+s5dD#%M|KUG&-8>)X5m4En3!e;D#qCj*J(C`PL2GXKlkknqJ<{MzNwQW%> zGE=5?HZg??m$JI;z=CkS-9axo@I5FfSO;IWjh@~M=*Y1O(fq`TFVoYnh`!o-)vLbS z#Ghn7<)UG035v>_aCjYfd!L8L@B=z#8t;F9cAF3w18aHt@-t8g^h)3eytK7xV?LqK zd5Y2sNHmC#W#|+DQcwpWHEM9+PCa_~FjLMo2`LF3l(8F%NunGzS7T!%sx(xIuiRWQ zIWyEC@BWs@<~gNmd3YA-iCOrqMuYcv>gFBgRaMrH+%x`DK;c*Y!>*y3kMCfa#4ZGq zN;|j-z#XRSJr`$;^I7h*lx_$DD(_(Dg;E`gw%t2Vewq%?Z(ICfCqP^Mt*P&up6LV7=Y4-!=rHvf6sesEYYTR~MWE=y#H! zeW74<$*e3a5Q&6kWeF4Cyq{u-1K6l5q-qG91HEp}+0|@~2 z{k7ASVdzdw$SomnWhNS`P3DA%vCghbu~Px3B#9!^j!)lFd)l(e^-&e7ioM%=Yo8bg z&%!mD)UBR1)=Kn5w1rd>v~snzi!my-Rsya3XDg4Jm|*D`<^*O++ekWQH|h)Cx(4z$z(zWyLT}G=Ogj0df)3SE&6|D?XfcH5dlJu>-l@W%&0M(G#fy?}V%8pf;T5#l!IZ$PSz}YiPB#!181zlbexP#+{2Pmt-S#||6G1M zu}fGwc7wIF>C_UPj7b44i&$9UJ~4yl>7U*T>u-1o8J43(rZ0Wx>U~GD@-lmU38c2` z*RNxIDs}fM-Rg}=u2chovkh--m(AYuwA^|pOb}u$O~mS7C(slpD^jA>mL{`Jid;Aa zgLRH0%Rr14vf%7C>iQCLj#|bbjPF!De{hcj**209%jbv+k?BNk(oeL7Zn=zs?Ur61B^xi z*ns&{dcerpBjGFW1HPV{`$xNknuJVPGLrP;$?b)^i5>b(a_OOy7mS<2&5BH=ZR_76 z?L(RL`n4sL0KL6&(wWK0lwDR{gNQRd)95>+RKXJY&mRI}hb z8_bEm8t}F&XHT~LUjo<+S|72Qj;XmDT|Sx7u3L#{@(3}a z4)a$wuuIw%$JgTY{pU}t`*ualgSldMULN}Q2Y$<*6tDSR;^Gw#AJPY*rjxcP$G+Y` zG!CfC4(aPJqrZEv6|@Oyf>ka*FYgou+M3%AQUU&cMR|D-P{9D&DiX#vB8o@_6)BKG zB}7n^tcprXl-eCqld&;D0yy+(J3ERU1CTomkwY|o#m8s)@82s}KU7mgng4*25(rmQ z(^eIgr`xwsj+5`N&oz)OfD402W)(5-qX9vHv#jw5)W1ND@Hffi1SZW&7}UCs;pX0d>lr(`=C} zM|WDW6PS(of{}el8EDQ%pCS)Q1dAIOepIj<+1#)xg*xu0wfVWO=cL}Ld@$<}zgPcH zv*V1hAtf*Dgd`NazlrSMg#cSuTIvQzRPKe)LnUGDp7|n3_ugwZRD$% zw^T#4NF5M>$ernb@$4B@%DKvA%l98A?4EeD+~EDdq^Z;V@pJ6U=N#J)rF;Y1f>u&A z*@KXQ%m)Sa2L&Mk0T$uyoqGXLD0)t`;n9_prb;Wo9uGx0$^P+n?ASU;<|IV)`-g{< z*vzdr#_Q1%i)#*6{)ZFDbG@_C_0E(5NrJMI7ue92tRaviX#6mQ1_d7aY%qBWKNzia z4wNnt-ynZP!#}XIBzE_ETLYdJR#_fh4X%0O#?^Q?siSSUlY-BHydjjq4ml$aF5g6>O(-8r4ma83GXOKt%7PHJ*dsg0 z*KaQi5p-y~mN0Yxe{UK~IsdWKxTbv^y(!o{omsHSh*rQNw}#n6GXd!XrutH$9ET)W zLb(X49(-Pue=G#*mZd($O|%&KnoScAR&Nx0z#D39D3O@hO4CS8$c^Nz(rvl9<%--T z!fsi`h-9|Oe>(w2140N8+|0~TC_5morQ}@UMSE5Ev+_OF2Ed0=!kLbh9-%)TerLrvOzmkLpFeHe^Hel zQ&Wd`Sl<|RZ+X>6CB$C*gWOd3MHmB?MEQykH+?zkB=pbSX>y(I?c)3Q3(^^Ak9`3Z zie=5%)piSA^%lu&pkY*vV1MGO1dXk&{jfzipDC%`LF`WCia6@>O8`so)DLH>)NDTt z4EKSq0TjppConBTfRKAva{-$sWBBBXkv{xXZ6y+gF!YEc)y6)9*G-j;u7|)>YdZx# zw6zsJ{7mk)xBKhLX3*^TGJ1P&Wo5Bbh9IFuE-Lo7qPwQinS}~^9fBCST}wrNZdCRL z;!HeCw!<6^vdo=3uIAn;e1KJsE6xB@nZ6x-fW7KU+6XzPB`(Ek=ESi{U9HUF`A(6q zUfha(=4RiuXN<7!(KrixE)<~l;`+(pdlPz6D+WQ*v*CY;0o6eO`)dp<4> z2o=Z`Oda7y%)KkD4$T$YnZ#zBtn3^lOXNxH@jKJ@nu}mQ3UMnXW-Wc&7;?bx5j7=o-<6ZWbfrKV8q^v92ycO4LAr>0P0nJ=C* z$g5VK!EHle5kldFUkzNwnMQ}z2ZKP^2YL4{C3Ay};FhwWG_I&)@vw@Y?9|a3P-{+^ zI(J=EpNWtddvcv7?qw}GfvhHV&cZ^KUG6JAEwu)+$xy)p9|Yj|gVRV+YHZcLfmE!% z$I;(^HTKu60Na7F*Pg68>t;r;BW4ErrZV{$XhR^^^ItS-_5wnhM!R>|o#-7JqL8a- zMb|MnS6(hty8Xwv|L=^31~t#eA0{U+V%|N09Qf~hD`G7`@PXHqh0Tup2qWs?MCdakXq|1}u zK)g@S6s;)tYlZ|xY}c;9Kd>Uo_@E4MX30U@QoZSAj-hZ<`Tg?Wr$@Ud`MxdLY(#7dGN%cOjy?>& zWL1tX_!9N{Pn%=M8pSU6GSyfY7@6kKw#e}2-Qt5vODxhMdm?gj9_n40ZgG~ay;h6@ zoSglr+z6OO0LON}p3!(?<-@X)l2No%k#1&Jg8(hHU`ussXlTK9=lujcQIN(1K*Cd? z{gi(ji;r18*cTldIvJ(zDoIfdqqh^nG#rZNlS%%7N)bBwpg=|T!VHSFG-j}b(1SAA ze%QafQg%HwlrEc_rV&A!;&_9^gY*Vc(K+ngKZm`y8dJ*3m=urMF+8@8@pm(@6Q`)P zpOzcE_tktt^&Vvppw&YM4{p`(@98-QpzPv!Hj}~oy9(0wZBn<0nR|Js%%eF|jK|i; zzhql(lVR>WcKs4f5F!kq-I5YX{RD)Dvnjh%nHx{F5kXXTLvuPL;y3R`HckRDIg?=V zY=QlQ$ti%m6!|QcD^mo&5{VDNPG*iSCT^wq_V2A*FB7ja0Y`-l=lvV&PzM7EM`Q`1 z9xit_St!1~(Ute2#cN6Y^2lL>_rTYY{-9&SJoQa-yVB)XM~ny6DyGDlrae+7Sz^fg zOhLsjwh4sVrx-MozO#pz7K!PGIyxL_m& z*!0YY=YqM5L_&7GOKj<9!YI)B`?~~k4Jb%DW^Pb@9g{zN`>F6_qlMEgr);f-Vp~6` zbM;6LSjYWb{wHzjtya9$yz$Bzp|Ur2hl}MT3}(;v*A@807C+eEp>n*qttlnD=vYFf zko+d8rO#@YwJdldxM_%(PY9-MG_;ZBD$czkPD|aaRC%=Ka=EpV2^$lf`?mPUxHj#b zK57)F!yV!@WKGf-~e0vKs z)0*w->o#trxk2Nk&;sBihIiQEL@dqb)JC!PD`zK36gCc`xDjxucc z>f^x~M8zpWY_l8lR$8raxqbZhM=dt3ORfil=bNXMJY4PjSKUN!wqxL11_@wfWCT$h z{v>GbP7i6fNUm_ysumV!HmW1mq1T0a^XaxZEx)F z_r$;nz`42pf=-Cm;B$_T;0L^{_-o6w01+dsX2|gZ+(Jpxb>rn_vX&Arm)2;J;85iI zlOFjBRHfNIDWu-B@vwxanJL3ojO)-OoEaUGZ`#=L2#B zU^>8^X$h#Yi6D$vfF^}33*{YpdjvWkisO(BEIewgDPxXk&5n{Nb~c2%{C5H_O}n_G zhK^hl*!EtbO!~!keVh}t1rgf7jDP+>$kS>9noJRFfqKJ856pf#+S^n2p5dJPec%R< zhUAs|Hcz%E8c@c73SVIkqPGaMv0zemUV}Q{%CYG*KLsekM`b2(F@Z^X`GWbgmcH9? zQXppb*Fp>JciyzZ{UrUQxf#962eadZ|DWks&t*gy9tURa9?q5EVhEixusggi%tK#W z>+w}+u<7Bq+VI@QL6RrP)MFcbkF3RyomY@K&Gs_oyN^}jPBNplw8QBVcAE9IXc>aD z>C2wep1HY&CIq8k2&^+$3WRm5o3HjcMTa&dzj5VId1#0WX7Z4PRkE&zEL; zw}^`u0bHRNqXHeUmY)8(42lPo2a*VC8Au*6^-91xJ|y!ta7P3uI|x?rMIk#RMKz0o zOTBG+W#pE@Bw9+iMLa}B5C8bV!9hs=GFm^mp^@qAl*IXymq*zM)eu`a(CSR^9zf+q zw{2@~YC46}kApjk9t)r}aIaaV{Wz^ED(FET0ak$u9`fG^ZE8aAn%UA=Bb0o)u|hUG zE}Kh2KnYq`&-fNd(^?efY7JDTU94fI63sG)8t-w)m?5g2JbU(~RF~fni9`Yc;`haT z5M^>RUUk+Ju8UzNMQ}}Oqv^W4HC_+pwDFFk=H}azJ9kP^CVw$4!zbtBaP}+2I8m8LVbF300>#DW%Iak+#-d>S-y~v-a z+tBGmTKd7AslGk{@?hM*zC}MfJ1C0{)8(U^K4dpwNQv^la;U1Pz|-=&b}oEn2|L(+ zL5PUkdsd#mJ&gknA*H3!+M;J%TnxyZrki0S^yW=ToNApOB+tkBiF}h;P@Yr-D8{cx z*9)~HIJbSJg{JJ>VcKi0pFo9*TAqK!L6j5yc>sNMW=5(Y`^unO8DC56*eZ?gunDT7xxR`k3c*|1y-3dj^ zLZP|CtfQmsYlzI2fA*-w#z3ZI0s$gt#9&`v{O#K;TW;g$phU+nXmxr}T3U*!u6^!0 zztHbu#1&E*v}TYCzpeLwf&v-vM07M)gf{N{1rHCf77@|Wr4XIoyZ02s*zo5A_ww%2 zGh2Nq-60dFB(@wQjGgA+y?gn8HpYmU(4WIzjFI;WgxavDfM-VKYlsL|(b_$-`IbzP z8^Zqs)EUwE?VE?0*-!`_GoV=z1UUZi*GoC%1SbjPL>Zo!xO(Rm-hy@h3aa+JDuEuF zBJ#q(cjOTo%|BiH#t9-UJRFveIM(V1$vB0`*)IAni+hx)r<(G>(U)xG#HoA%qf*2(@{6c@SLS0m?cWz+b~! z8XM&yox&_CZ+M?`73Hg3t>vn)VA+DvVnOnM-PC_n&8Ma8I+ZjRR6rEAtxH7<_LHt8kE=QZT|29U+2O;u zXTcn~EKi<1F6i-)V7!HygVx3)GKYjm$H;9@7KoS)} zO+!POR)hu?(v@d+aEN_09xy*UtLWIFP)!9JYGhAS0Cc((^69rcF^?@1pIc$H5kHEzA$woJ)NIB3Op&cAsjt= z6gddfk5f|={ua^$VaCr(>(BmQNoO99W&XYKI}Fk$A*3uJ3K^*=mFy8AYuY9%CWE4# zs5FI|_)6MCsX-JaDJiL>riCm~jZ~&INwkvkd*AbW{^(`WJkR~y_vdrYbqDPF z>7MPM9}Y|o6BXD%89`v~0)yIeR0maMXh=wHO^xV5MzaG3VxPD3_o?d02`N$5S^g;b zV@pNL*Pb4fh8fBJ#%bHPZ>RA+`rvn3L%UpNV+%f7(#$CLzV zILo9T2lrvB0Yis(!;*I7!77Rewh&eM)s}F&cp~j9Hy2&&Xhk_Pou2EVeK?+qrew1H z`_IgJS{!y1a!5|j?OV4HY@i{;$(NZ53g^0q4+9E(Au9qip*HI%v;iBfGu)h=bK!Di zxpdweD1d<1S(eeu7&Atudqt`2*7B<_Iz^i{<;1-F{7qiobZ5y_r%M{37{O3_a5gd` z0te==2edlwq1pHjt;`NI5A(VOQc9sb=nmY@)=sj8Z#mjV?SPoEQ6Sq8?c zVZ<4G2Zs;qEV;=uq0xyo5c>PFvgyf=k?=(K&;Hz5sWDey@jtQ}Q8Zcd-q$!s&Je&{ zte?OH>DIBOupNb1*iOP#W*$41EhG;p#0T=XK*k~c`$M~4pZimcF6|%DK7f(-)~z;v zeyVZdfq~Z_K0J$~ZhAVBZmNWL^lxzx(1?0p4*i06hsHG>_AC*@$HU)$YS`bF+Q~a= zofAIz8hRTlGwpyz!Nf*g&{R$oDcXk*N7E@|nm>K{Tvjs+C&;Xch?LyBoQ~{kXO_;j z#U`stdV%lQwt-jJ%H&Ui*L+68Tg%-7!Q)Jvc(XYfZpgY}#GUaC;f)Y2;f=|7z4!Zf zcPHEiKjM2hWOwBlU0qc*HO9(nDk^wM(_h8j%~oHLQ*l*<#UUYNFjOeoM7uxU*2&eC7>G|NN_&_g^v!~U zf>7XfLSXCanunndzBiyZG}?v=@l-+W|NOItp}_U)*YTheou&*7W;r;FL4*GIu|Fu_ z{wru3Nq1VsJ~FR5@imbX&`1gNZNp9to#!uFAH0CNziJgzCK+jI1b)sLZj>a*<;k_o zR8$9YOjI}nMB0N^n*8WiI_1S6q5&oxu(CX`!oW3i-oAeAsFy-nvgvD!%-XeI z=mg2f+fF*TVD8)xTrgN)PHGF<#g)1+0D-X#-jNeQz2oF3YxDCb1jh6|)eah>rP9=?<9at& zADGi}JE8X5>7kBSmtR_taN{}69%TNo>xfL~`4_`?O)wM0gQI(_&fO-Tk#ih8Hi2z! zEJ6JG=FJwO{xVn4fr?k=tK!E$oHhKAa^oDFF# z#5;G7^q{;ykC1msxWuE^^B@w+W*j-P(9Z70+UDr(zeW>^NeXDhB!veF*qR8|I}WJK z3TB2#dAaWYX9q*$pSmA90et~j(MD(uyMUMC_f+#bK^QJE%XX%x#qP=xYHC~L@iNru zc>bXqI*WGl`|haCRZwV zDKIT^$#BDw_viB_ATr(CDHSb-3ocUY?oCT0ZNBgBrOKC?Nt3!m-x z8&zYI>17c$sdwz8u~x;COiYN-VSiu54V@KwPP*p71FMHJ$XDrEO`RUSlHATAKmA21tRQJ4X!yFx}tQ*gX|n`H!viYKOaYgE#qK5z)EKo_&2u zC&`WgK#Gx-rH=CS^0Eu9@Ep7PR1Q`%OUbj`bLLzF=GJ=_fysYB84j^mZ z>V=~ZGn62PfIaryX^oi?TM5{qhH$B!#wD%-2yi7O zz(~m10mmeEE)5TV@bF=Z>q+t*Uvt+W%@4!5_q7cT6gwM#zK&p-ka|C<^xN*`>v(L+ zi7<#ynny5or%!W=@kz)_4YMyjGGF3mrazMt1okLf#ZcKA|pOk8E37fR)a-4 ziXqV+A#1+z+EXL7=4Dgo7~Dhop6nR6Dg6)#zi{k}&UHagBWNgK(zFNH0oY&F7=KwP zmnOgtMCnNTR7yCizeBQT1H!Jd&N##KJauYd-8oEW*r(^tDb9K%7S;&~ z8#Zp#iX-h985={~BDFJt3~2SCF~&ahP{#upSuN__&6}Bl$!W30?CMu}jxEZ}l(636 z*_^OuBYZesU?Cy4qNOy)o+v2@eCL=Sy`%uJ+Wh%X9zBxP3Ow6Lgaea?OuEEK2CU?I z(ndDeY#u#Tc&w(kh~b%T`eY0FYXBC2xs9wpLeRuScSltzQ=_@H$@gD@gCYGK(<93B zt#H)POVL_!1(uU+#=(RBP`aKQ;CII9kBXK9ygz6M$wCxnMspx7&4=K{@RP^)C}a|a z?v5Rfvt}6>89@ZHWDZ4tW~r$k!GeYe?#uLYwc=*$eW0~QPo-$)R6bN z`R-Z@GtYW@ZmQMe+k><;{xcO`5**wHrlrUE?b|ns`O(_iL@&)ahVk~v6Nk!G>}s$~ zU#qB>(%m4oKx4X_np+nruyE36Lh^aq+{|H(u;)1%;;#b}MWY}7lA8f`j;70c zbuS~xvN7Q`g4W*t`|oIlcs@m>9Pr5KyCO8DXj@XoF(6drL@l#5YkIZmPf$7pm4bw^ z56aEX{@KhdPM$LqLYm8v_80n|VrY&HOl+(mTqOc>p@1-I^7`s(JSBA%L~|+Px3lvA z%wWRJ`AEjf0b}lAj$0s~;gKzBN+o7GI8doG9t7=!xmXPFnqX$`vChEZ?72n7ZR{0| z=ba0pu_FitUsJ!$%(UMs-?h$CVl%EomgeSP%Vjj&vOT99hE`A1B6|@}sh0^7cp!UB zEGjN0g#e@SeG9;ck`^&Y9h)z$9U{2-_z)0T53eJ85>H=$|K23oQGL{?!2<^}qqT%d zkJBUxpE4SfNSdKvCZM0Di9)=YnVI5T3!+0tMl#kbZ6}YG={*}RXfhT7qU*nGz$c#@ z0bcTe$oayWDkJQ9ZJNf0SS2nW! zrLM6Wp^0{SM9ew2^DHTT&HO;qZv45~>74B|;IQjj(CXT?i;8|#YO-`*nd+vG!=Ji} zif}*jWXG@suWLtH7T;S}^vT}t_snIOy83)KH#qNqwTQII{j>9Pb8QAxpbci9aC}s4 z>ch533fA`akqQb_9ABf#AQXVx)>2gI9Fr2#Z^aNMiMIS15Ei!q7pyaQ)-nQCh^LlB zfpd=zboTM%FS$mF4Uv&fmVo#|>Hjg);Nn}asUv1cZm;n2@!?=WwOy0U&5`*f1P&!e zQMo3LFWU!$h|Fhi|9er<#8f9cpF9>+U7eSvJR$NYDJgS2&CSitCQTA8SgkbOvTHfi z134pix!{Y(&gE1G^a+r9+`o@a>^L1A9ooXDPKCh#2h~STIb6{~!*KXvJC%XI;}{Fc zXT|x=cDp{q-C(m&G;jLR{R)c5-Mhbqhs#ephPHOl;K6k5`t|Q*le}Noi>pHHK#gxb zJ(wwAi16>LS81uK+n(4`c!(IJd;5(7&M$ezt>h9 zSsXKoaktZ9D_<=y=Y)=wyUVWp^Ru6?@5|O!rfN*yXlX@tRgHPSKUEx}Qyi~csk-8? z^Qbe3_jW5ZRj)DYaZ90~KvadQ9lVl=!?T6SUxqltw7Rll}6 znr)$>`RvR`cjz*7uCr9|5`}XU>s0T<%l#gDpkNs%bY{5WNK)Ma#0iWx22+dyF}T}+ zwe<55RY0p2V(B1(vCP6Vi-O7c(b3Zm-lgyhlDzZk#P0#_gN3jIu-CAmM@X`s>bS3` z;R?f?WPND?(h{kN0&wT~7)fELQgmjpdqhM;K)~px3<&{P%8>Ej7cV&X>&U-dZhK@u zx-cEt9v6q7DIPdErKO^c4w28XF*V&{T>J20#F{lY z)nffEcr*hMk3G)V>uEXK^9(8($kQJ+9d&9j_9O4iKwL-z4aV?eR!LLS-qh5_#zu@V zh;qfn#m-0DcU3=matPP32M>fJpGwDX33bp;PWl)_j{)P23%DOFi6s>QQG{3u(9AW; zi8msrjP#;6!I8pLk@Niga280P>}F@T6xkL^0milg#qR`>&(4t{uLeqw2B0DuptPp; zTCiXm5m8rH{KsrQpc@@W{9PLA>Xx2uOj!O4E}ltA;f!d6tIkDw9a1qm!ta>oJ+dkA z?Y%L4QMv|wX{*aoZ2Be29E&|$=VB>mhZJs-5}0&|K#UeL+%kMyv7VL@h>ptx15td8 z)=(U^n}9mSI1SHb6-yZhU(ErVgbOQeK;IP=HipV4Pn;0>qJRDg*oIdNLvFn& z+Y)43=*TC>-(aH=o+g-@2Ab|?wJ_Zv)nRu;t4k1AI6x$BH^7wI8N7;xg4S)sq69E3 zCa8jZd>Yc~|12&B?t5jJ9edb@D4{+4?vesD6 zVzU*-%_=Gg)PCMSn`uX8=Cwdm!XL?>7|EZY$eO<;1t9453PjwTMab1vzR1{h7Cnz< z90olEG{=_u#zy>YDA>;U8g4D$H)ZwM12?<6dx2Pn%B0aLOKlfJ@0#^_g?Pp>(`n#<2_gIpKY&~vS@tv+^)1L zij6sQI^Mp;+6nK29I_^~TjV)#A!YOb9Zf?4Dd zP^K}EU}gY#@}?&h5P(Tue_CiCi?%F(oWN}-xb3jjrk>-8j?vR2(f8rFBgdKO>SkTMIAy|wVQsGR;xPK1 zGP^yaItY0_wFIWVy3w?jcI?-1h=y81LNuF>Cy0@ z0MOPj;7qsn_FY>(Uf!`vST~?lZ{O3SPSSmCPan0YlyuKHQo^iG-E(YRWo2beO&4@! zy3>%Q^28_Ixvm%QF^WZm42%nsjt^~fb+dH8(Lw`~$jz)AZ+nR&ZAmvxv8UmDy}xQ| z6a*8HFjW<2r1TTS84L@N0zqs!!;uOs+&#VLHT^p>ICO#kIc4cT=%7v~m?X!^w0S0F z%37O)n*IGBHw5YZJvkz`a}M<}Wdoh|P2WHH$ZEyg&O-|tanF08bOj#DDRZVbUWw_; zjY_5rjhZrr8*ZIg>_1|vDQ5c<7ej#}pKJDSwUc$pR$ps+N4shtdxeW1ov012r{4h+GIUH+U%+2qR&D z1dnI%_Hp|9R_hr)!1~vhIG3H93%bd4)o9;^W~RJ#)r_9kqql9QbPn_PKS%^)%u`Zg zAqZQ`ds%F-T@959s!GAbV4e6VhCN{p4j&$Pe`T&s*5GF@TMQoro2ey8U`2>qMsmcU zqxZ)f8QB$z~1b;^z z&X}=N-b^H-fMDRt_oc-{ogQFBmzB7T)A0RnWqtRq=D%PZh%j0d1NU~XytJ+2sI`Sq z>Yg+^{8YH~{_V$9{KfIb)n?jhjD3B4CWC#ExdU%+RZ9x#gg`zce6XfMJlD-bPf(dI zS6f#H3xOGATjZ2@+a6926v^K7zYu}<6Hq2>D_=Z+!2;?HEYL2@f0tNoy~FnTibKm% z^}EBi+G=kTEji)YPmUt8;EiS+D#2*6;W}k4)RDjqx=@-@1sV(9gmzpz6??p$Qb#Ng=s$OQ8~7 zzaGdOSW_O~%9pCSd3k(mp4(3K#H7j;1l(Pg3h|r)Hw}cNe{Hj^HX|;`T+piMrQcjq z!1?2pHjz^nU_&@!1hb98#Svi$Ho{zFkLZM{4P5^WYR^_L0^`> zjKDwC?;iitx99Id`N=)=Use the contents included in this guide and apply the security settings detailed here at your own discretion.** + +# Intro + +This section is structured in three main subsections: + + + +* Enabling GeoIP in NGINX config with the purpose of filtering (blocking) web requests based on the country’s source IP. +* Enabling anti “bad” bots/referrers in HTTP requests to the NGINX server. +* Compiling and enabling ModSec + OWASP CRS in NGINX server. + +Each section can be enabled independently. + +# Hardening NGINX settings + +## GeoIP Integration in NGINX - Blocking Requests by Country Code + +Install required packages and NGINX module for GeoIP: + + +``` +# apt-get install geoip-database libgeoip1 libnginx-mod-http-geoip +``` + + +Verify that the GeoIP database files have been placed in the right location: + + +``` +# ls -lrt /usr/share/GeoIP/ +total 10004 +-rw-r--r-- 1 root root 8138841 Jan 24 2020 GeoIPv6.dat +-rw-r--r-- 1 root root 2099217 Jan 24 2020 GeoIP.dat +``` + + +Edit NGINX config file (“/etc/nginx/nginx.conf”) and add the following config under the “http {“ block: + + +``` +http { + + ## + # Basic Settings + ## + # Load GeoIP Database + geoip_country /usr/share/GeoIP/GeoIP.dat; + +``` + + +The next settings will depend on the desired GeoIP blocking strategy. For “allow by default, deny by exception”, the config would be: + + +``` +http { + + ## + # Basic Settings + ## + # Load GeoIP Database + geoip_country /usr/share/GeoIP/GeoIP.dat; + # map the list of denied countries + map $geoip_country_code $allowed_country { + default yes; + # BLOCKED_COUNTRY_1 + COUNTRY_CODE_1 no; + # BLOCKED_COUNTRY_2 + COUNTRY_CODE_2 no; + # BLOCKED_COUNTRY_3 + COUNTRY_CODE_3 no; + } + +``` + + +(The macro can be modified to achieve the “deny by default, allow by exception” approach). + +Finally, the following “if” statement needs to be placed in all the vhosts where the GeoIP blocking should take effect, under the “location” section: + + +``` + location / { + root /var/www/rmm/dist; + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; + # block the country + if ($allowed_country = no) { + return 444; + } + } + +``` + + +The HTTP Status = 444 is a good choice for NGINX not “wasting” too many resources in sending back the 4xx code to the client being blocked by GeoIP. + + +## Blocking “bad bots” and “bad referrers” + +Nginx Bad Bot and User-Agent Blocker, Spam Referrer Blocker, Anti DDOS, Bad IP Blocker and Wordpress Theme Detector Blocker + +Source: + +[https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker](https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker) + +Download “install-ngxblocker” to your /usr/local/sbin/directory and make the script executable. + + +``` +sudo wget https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/install-ngxblocker -O /usr/local/sbin/install-ngxblocker +sudo chmod +x /usr/local/sbin/install-ngxblocker +``` + + +**(OPTIONAL)**Now run the ”install-ngxblocker” script in **DRY-MODE** which will show you what changes it will make and what files it will download for you. This is only a DRY-RUN so no changes are being made yet. + +The install-ngxblocker downloads all required files including the setup and update scripts. + + +``` +cd /usr/local/sbin +sudo ./install-ngxblocker +``` + + +This will show you output as follows of the changes that will be made (NOTE: this is only a **DRY-RUN** no changes have been made) + + +``` +Checking url: https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/include_filelist.txt +** Dry Run ** | not updating files | run as 'install-ngxblocker -x' to install files. +Creating directory: /etc/nginx/bots.d +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/conf.d/globalblacklist.conf [TO]=> /etc/nginx/conf.d/globalblacklist.conf +Downloading [FROM]=> [REPO]/conf.d/botblocker-nginx-settings.conf [TO]=> /etc/nginx/conf.d/botblocker-nginx-settings.conf +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/bots.d/blockbots.conf [TO]=> /etc/nginx/bots.d/blockbots.conf +Downloading [FROM]=> [REPO]/bots.d/ddos.conf [TO]=> /etc/nginx/bots.d/ddos.conf +Downloading [FROM]=> [REPO]/bots.d/whitelist-ips.conf [TO]=> /etc/nginx/bots.d/whitelist-ips.conf +Downloading [FROM]=> [REPO]/bots.d/whitelist-domains.conf [TO]=> /etc/nginx/bots.d/whitelist-domains.conf +Downloading [FROM]=> [REPO]/bots.d/blacklist-user-agents.conf [TO]=> /etc/nginx/bots.d/blacklist-user-agents.conf +Downloading [FROM]=> [REPO]/bots.d/blacklist-ips.conf [TO]=> /etc/nginx/bots.d/blacklist-ips.conf +Downloading [FROM]=> [REPO]/bots.d/bad-referrer-words.conf [TO]=> /etc/nginx/bots.d/bad-referrer-words.conf +Downloading [FROM]=> [REPO]/bots.d/custom-bad-referrers.conf [TO]=> /etc/nginx/bots.d/custom-bad-referrers.conf +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/setup-ngxblocker [TO]=> /usr/local/sbin/setup-ngxblocker +Downloading [FROM]=> [REPO]/update-ngxblocker [TO]=> /usr/local/sbin/update-ngxblocker +``` + + +Now run the install script with the -x parameter to download all the necessary files from the repository: + + +``` +cd /usr/local/sbin/ +sudo ./install-ngxblocker -x +``` + + +This will give you the following output: + + +``` +Checking url: https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master/include_filelist.txt +Creating directory: /etc/nginx/bots.d +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/conf.d/globalblacklist.conf [TO]=> /etc/nginx/conf.d/globalblacklist.conf...OK +Downloading [FROM]=> [REPO]/conf.d/botblocker-nginx-settings.conf [TO]=> /etc/nginx/conf.d/botblocker-nginx-settings.conf...OK +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/bots.d/blockbots.conf [TO]=> /etc/nginx/bots.d/blockbots.conf...OK +Downloading [FROM]=> [REPO]/bots.d/ddos.conf [TO]=> /etc/nginx/bots.d/ddos.conf...OK +Downloading [FROM]=> [REPO]/bots.d/whitelist-ips.conf [TO]=> /etc/nginx/bots.d/whitelist-ips.conf...OK +Downloading [FROM]=> [REPO]/bots.d/whitelist-domains.conf [TO]=> /etc/nginx/bots.d/whitelist-domains.conf...OK +Downloading [FROM]=> [REPO]/bots.d/blacklist-user-agents.conf [TO]=> /etc/nginx/bots.d/blacklist-user-agents.conf...OK +Downloading [FROM]=> [REPO]/bots.d/blacklist-ips.conf [TO]=> /etc/nginx/bots.d/blacklist-ips.conf...OK +Downloading [FROM]=> [REPO]/bots.d/bad-referrer-words.conf [TO]=> /etc/nginx/bots.d/bad-referrer-words.conf...OK +Downloading [FROM]=> [REPO]/bots.d/custom-bad-referrers.conf [TO]=> /etc/nginx/bots.d/custom-bad-referrers.conf...OK +REPO = https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master +Downloading [FROM]=> [REPO]/setup-ngxblocker [TO]=> /usr/local/sbin/setup-ngxblocker...OK +Downloading [FROM]=> [REPO]/update-ngxblocker [TO]=> /usr/local/sbin/update-ngxblocker...OK +``` + + +All the required files have now been downloaded to the correct folders on Nginx for you direct from the repository. + +**NOTE:** The setup and update scripts can be used, however in this guide the config is done manually. For script execution, refer to the Github page linked above. + +Include any public IP addresses that should be whitelisted from bot and referrer analysis/blocking by editing the file “/etc/nginx/bots.d/whitelist-ips.conf”. + +Finally, edit every vhost file (“/etc/nginx/sites-enabled/frontend.conf”, “/etc/nginx/sites-enabled/rmm.conf” and “/etc/nginx/sites-enabled/meshcentral.conf”) and place the following include statements under the “server” block: + + +``` +server { + listen 443 ssl; + include /etc/nginx/bots.d/ddos.conf; + include /etc/nginx/bots.d/blockbots.conf; +``` + +# Enabling ModSec in NGINX + +All steps in this section taken from the NGINX blog post “Compiling and Installing ModSecurity for NGINX Open Source”: + +[https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/](https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/) + + +## Install Prerequisite Packages + +The first step is to install the packages required to complete the remaining steps in this tutorial. Run the following command, which is appropriate for a freshly installed Ubuntu/Debian system. The required packages might be different for RHEL/CentOS/Oracle Linux. + + +``` +$ apt-get install -y apt-utils autoconf automake build-essential git libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre++-dev libtool libxml2-dev libyajl-dev pkgconf wget zlib1g-dev +``` + +## Download and Compile the ModSecurity 3.0 Source Code + +With the required prerequisite packages installed, the next step is to compile ModSecurity as an NGINX dynamic module. In ModSecurity 3.0’s new modular architecture, libmodsecurity is the core component which includes all rules and functionality. The second main component in the architecture is a connector that links libmodsecurity to the web server it is running with. There are separate connectors for NGINX, Apache HTTP Server, and IIS. We cover the NGINX connector in the next section. + +To compile libmodsecurity: + +Clone the GitHub repository: + + +``` +$ git clone --depth 1 -b v3/master --single-branch https://github.com/SpiderLabs/ModSecurity +``` + + +Change to the ModSecurity directory and compile the source code: + + +``` +$ cd ModSecurity +$ git submodule init +$ git submodule update +$ ./build.sh +$ ./configure +$ make +$ make install +$ cd .. +``` + + +The compilation takes about 15 minutes, depending on the processing power of your system. + +Note: It’s safe to ignore messages like the following during the build process. Even when they appear, the compilation completes and creates a working object. + + +``` +fatal: No names found, cannot describe anything. +``` + +## Download the NGINX Connector for ModSecurity and Compile It as a Dynamic Module + +Compile the ModSecurity connector for NGINX as a dynamic module for NGINX. + +Clone the GitHub repository: + + +``` +$ git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git +``` + + +Determine which version of NGINX is running on the host where the ModSecurity module will be loaded: + + +``` +$ nginx -v +nginx version: nginx/1.18.0 (Ubuntu) +``` + + +Download the source code corresponding to the installed version of NGINX (the complete sources are required even though only the dynamic module is being compiled): + + +``` +$ wget http://nginx.org/download/nginx-1.18.0.tar.gz +$ tar zxvf nginx-1.18.0.tar.gz +``` + + +Compile the dynamic module and copy it to the standard directory for modules: + + +``` +$ cd nginx-1.18.0 +$ ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx +$ make modules +$ cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules +$ cp objs/ngx_http_modsecurity_module.so /usr/share/nginx/modules/ +$ cd .. +``` + +## Load the NGINX ModSecurity Connector Dynamic Module + +Add the following load_module directive to the main (top‑level) context in /etc/nginx/nginx.conf. It instructs NGINX to load the ModSecurity dynamic module when it processes the configuration: + + +``` +load_module modules/ngx_http_modsecurity_module.so; +``` + +## Configure and Enable ModSecurity + +The final step is to enable and test ModSecurity. + +Set up the appropriate ModSecurity configuration file. Here we’re using the recommended ModSecurity configuration provided by TrustWave Spiderlabs, the corporate sponsors of ModSecurity. + + +``` +$ mkdir /etc/nginx/modsec +$ wget -P /etc/nginx/modsec/ https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended +$ mv /etc/nginx/modsec/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf +``` + + +To guarantee that ModSecurity can find the unicode.mapping file (distributed in the top‑level ModSecurity directory of the GitHub repo), copy it to /etc/nginx/modsec. + + +``` +$ cp ModSecurity/unicode.mapping /etc/nginx/modsec +``` + + +Change the SecRuleEngine directive in the configuration to change from the default “detection only” mode to actively dropping malicious traffic. + + +``` +#SecRuleEngine DetectionOnly +SecRuleEngine On +``` + +# Enabling OWASP Core Rule Set + +Clone OWASP CRS: + + +``` +$ cd /etc/nginx/modsec +$ git clone https://github.com/coreruleset/coreruleset.git +``` + + +Create CRS setup config file: + + +``` +$ cp /etc/nginx/modsec/coreruleset/crs-setup.conf.example /etc/nginx/modsec/coreruleset/crs-setup.conf +``` + + +Edit config file and enable a paranoia level of 2 (comment out section below and modify the paranoia level from 1 - default to 2): + + +``` +SecAction \ + "id:900000,\ + phase:1,\ + nolog,\ + pass,\ + t:none,\ + setvar:tx.paranoia_level=2" +``` + + +A Paranoia level of 2 is a good combination of security rules to load by the ModSec engine while keeping low the number of false positives. + +The OWASP CRS team carried out some tests using BURP against ModSec + OWASP CRS: + + +![alt_text](images/owasp_burp.png "image_tooltip") + + +Create ModSecurity base config file (“/etc/nginx/modsec/modsec-base-cfg.conf”) and include the following lines (the order is important)`:` + + +``` +Include /etc/nginx/modsec/modsecurity.conf +Include /etc/nginx/modsec/coreruleset/crs-setup.conf +Include /etc/nginx/modsec/coreruleset/rules/*.conf +``` + + +Enable ModSec in all NGINX enabled sites: + +“/etc/nginx/sites-enabled/frontend.conf”, “/etc/nginx/sites-enabled/rmm.conf” and “/etc/nginx/sites-enabled/meshcentral.conf”: + + +``` +server { + modsecurity on; + modsecurity_rules_file /etc/nginx/modsec/modsec-base-cfg.conf; + +………………….. +………………….. +``` + + +Tactical RMM custom rules: + + + +* Access to the admin UI (front-end): We apply the “deny by default, allow by exception” principle, whereby only a set of predefined public IPs should be allowed to access the UI +* API and Meshcentral: RMM agents and RMM UI (as referrer while an admin session is active) make web calls that get blocked by the OWASP CRS, specifically PUT, POST and PATCH methods. These three methods can be “whitelisted” when the requested URI matches legitimate requests. +* Connection to Meshcentral during Tactical agent install. + +Create a .conf file under “/etc/nginx/modsec/coreruleset/rules” named “RMM-RULES.conf”, for example, with the following content: + + +``` +#ADMIN UI/FRONTEND ACCESS - DENY BY DEFAULT, ALLOW BY EXCEPTION +SecRule SERVER_NAME "rmm.yourdomain.com" "id:1001,phase:1,nolog,msg:'Remote IP Not allowed',deny,chain" +### ALLOWED PUBLIC IP 1 ######### +SecRule REMOTE_ADDR "!@eq IP1" chain +### ALLOWED PUBLIC IP 2 ######### +SecRule REMOTE_ADDR "!@eq IP2" "t:none" + +#API AND MESHCENTRAL - WHITELIST PUT, PATCH AND POST METHODS BY REQUESTED URI +SecRule REQUEST_URI "@beginsWith /api/v3/checkin" "id:1002,phase:1,t:none,nolog,allow,chain" +SecRule REQUEST_METHOD "PUT|PATCH" "t:none" +SecRule REQUEST_URI "@beginsWith /api/v3/checkrunner" "chain,id:'1003',phase:1,t:none,nolog,allow" +SecRule REQUEST_METHOD "PATCH" "t:none" +SecRule REQUEST_URI "@beginsWith /alerts/alerts" "chain,id:'1004',phase:1,t:none,nolog,allow" +SecRule REQUEST_METHOD "PATCH" "t:none" +SecRule REQUEST_URI "@beginsWith /agents/listagents" "chain,id:'1005',phase:1,t:none,nolog,allow" +SecRule REQUEST_METHOD "PATCH" "t:none" +SecRule REQUEST_URI "@beginsWith /api/v3/sysinfo" "chain,id:'1006',phase:1,t:none,nolog,allow" +SecRule REQUEST_METHOD "PATCH" "t:none" +SecRule REQUEST_URI "@beginsWith /api/v3/winupdates" "chain,id:'1007',phase:1,t:none,nolog,allow" +SecRule REQUEST_METHOD "POST" + +##REQUIRED FOR MANAGEMENT ACTIONS FROM ADMIN/FRONT-END UI. WHITELIST BY REFERRER's URL +SecRule REQUEST_HEADERS:REFERER "https://rmm.yourdomain.com/" "id:1008,phase:1,nolog,ctl:ruleRemoveById=920170,allow" + +#REQUIRED FOR NEW CLIENTS TO CONNECT TO MESH SERVICE WHILE INSTALLING THE AGENT +SecRule REQUEST_URI "@beginsWith /api/v3/meshexe" "id:1009,phase:1,nolog,ctl:ruleRemoveById=920170,allow" + +### NOTE ON RULE ID = 920170 (WHITELISTED IN CASES ABOVE FOR TACTICAL RMM) ### +# Do not accept GET or HEAD requests with bodies +# HTTP standard allows GET requests to have a body but this +# feature is not used in real life. Attackers could try to force +# a request body on an unsuspecting web applications. +# +# -=[ Rule Logic ]=- +# This is a chained rule that first checks the Request Method. If it is a +# GET or HEAD method, then it checks for the existence of a Content-Length +# header. If the header exists and its payload is either not a 0 digit or not +# empty, then it will match. +# +# -=[ References ]=- +# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 +### +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b484955da9..e922b219f1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -38,6 +38,7 @@ nav: - "Grafana": 3rdparty_grafana.md - "TeamViewer": 3rdparty_teamviewer.md - Tips n' Tricks: tipsntricks.md + - Securing NGINX: securing_nginx.md - Contributing: - "Contributing to Docs": contributing.md - "Contributing using VSCode": contributing_using_vscode.md From 403762d862f46e156a719adc9b143094c6c4bc14 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Tue, 31 Aug 2021 22:45:53 -0400 Subject: [PATCH 02/33] wip script additions --- .../Win_Disk_Space_Usage_Reports_Wiztree.ps1 | 302 ++++++++++++++++++ ...n_Disk_Space_Usage_Reports_WiztreeAlt2.ps1 | 17 + scripts_wip/Win_Powershell_Upgrade.ps1 | 2 + scripts_wip/Win_Powershell_Version_Check.ps1 | 12 + scripts_wip/Win_Speedtest_Ookla.ps1 | 33 ++ scripts_wip/Win_Speedtest_Packetloss.ps1 | 65 ++++ 6 files changed, 431 insertions(+) create mode 100644 scripts_wip/Win_Disk_Space_Usage_Reports_Wiztree.ps1 create mode 100644 scripts_wip/Win_Disk_Space_Usage_Reports_WiztreeAlt2.ps1 create mode 100644 scripts_wip/Win_Powershell_Version_Check.ps1 create mode 100644 scripts_wip/Win_Speedtest_Ookla.ps1 create mode 100644 scripts_wip/Win_Speedtest_Packetloss.ps1 diff --git a/scripts_wip/Win_Disk_Space_Usage_Reports_Wiztree.ps1 b/scripts_wip/Win_Disk_Space_Usage_Reports_Wiztree.ps1 new file mode 100644 index 0000000000..a6cd2e0634 --- /dev/null +++ b/scripts_wip/Win_Disk_Space_Usage_Reports_Wiztree.ps1 @@ -0,0 +1,302 @@ +<# +From https://smsagent.blog/2018/08/15/create-disk-usage-reports-with-powershell-and-wiztree/ + +To use the script, simply download the WizTree Portable app, extract the WizTree64.exe and place it in the same location as the script (assuming 64-bit OS). Set the run location in the script (ie $PSScriptRoot if calling the script, or the directory location if running in the ISE), the temporary location where it can create files, and the server share where you want to copy the reports to. Then just run the script in admin context. + + #> + + +# Script to export html and csv reports of file and directory content on the system drive + +# Use to identify large files/directories for disk space cleanup +# Uses WizTree portable to quickly retrieve file and directory sizes from the Master File Table on disk +# Download and extract the WizTree64.exe and place in the same directory as this script + +# Set the running location +$RunLocation = $PSScriptRoot +#$RunLocation = "C:\temp" +$TempLocation = "C:\temp" + +# Set Target share to copy the reports to +$TargetRoot = "\\server-01\sharename\DirectorySizeInfo" + +# Free disk space thresholds (percentages) for summary report +$script:Thresholds = @{} +$Thresholds.Warning = 80 +$Thresholds.Critical = 90 + +# Custom function to exit with a specific code +function ExitWithCode { + param + ( + $exitcode + ) + $host.SetShouldExit($exitcode) + exit +} + +# Function to set the progress bar colour based on the the threshold value in the summary report +function Set-PercentageColour { + param( + [int]$Value + ) + + If ($Value -lt $Thresholds.Warning) { + $Hex = "#00ff00" # Green + } + + If ($Value -ge $Thresholds.Warning -and $Value -lt $Thresholds.Critical) { + $Hex = "#ff9900" # Amber + } + + If ($Value -ge $Thresholds.Critical) { + $Hex = "#FF0000" # Red + } + + Return $Hex +} + +# Define Html CSS style +$Style = @" + +"@ + +# Set the filenames of WizTree csv's +$FilesCSV = "Files_$(Get-Date –Format 'yyyyMMdd_hhmmss').csv" +$FoldersCSV = "Folders_$(Get-Date –Format 'yyyyMMdd_hhmmss').csv" + +# Set the filenames of customised csv's +$ExportedFilesCSV = "Exported_Files_$(Get-Date –Format 'yyyyMMdd_hhmmss').csv" +$ExportedFoldersCSV = "Exported_Folders_$(Get-Date –Format 'yyyyMMdd_hhmmss').csv" + +# Set the filenames of html reports +$ExportedFilesHTML = "Largest_Files_$(Get-Date –Format 'yyyyMMdd_hhmmss').html" +$ExportedFoldersHTML = "Largest_Folders_$(Get-Date –Format 'yyyyMMdd_hhmmss').html" +$SummaryHTMLReport = "Disk_Usage_Summary_$(Get-Date –Format 'yyyyMMdd_hhmmss').html" + +# Run the WizTree portable app +Start-Process –FilePath "$RunLocation\WizTree64.exe" –ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FilesCSV"" /admin 1 /sortby=2 /exportfolders=0" –Verb runas –Wait +Start-Process –FilePath "$RunLocation\WizTree64.exe" –ArgumentList """$Env:SystemDrive"" /export=""$TempLocation\$FoldersCSV"" /admin 1 /sortby=2 /exportfiles=0" –Verb runas –Wait + + + +#region Files + +# Remove the first 2 rows from the CSVs to leave just the relevant data +$CSVContent = Get-Content –Path $TempLocation\$FilesCSV –ReadCount 0 +$CSVContent = $CSVContent | Select –Skip 1 +$CSVContent = $CSVContent | Select –Skip 1 + +# Create a table to store the results +$Table = [System.Data.DataTable]::new("Directory Structure") +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Name", [System.String])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (Bytes)", [System.Int64])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (KB)", [System.Decimal])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (MB)", [System.Decimal])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (GB)", [System.Decimal])) + +# Populate the table from the CSV data +Foreach ($csvrow in $CSVContent) { + $Content = $csvrow.split(',') + [void]$Table.rows.Add(($Content[0].Replace('"', '')), $Content[2], ([math]::Round(($Content[2] / 1KB), 2)), ([math]::Round(($Content[2] / 1MB), 2)), ([math]::Round(($Content[2] / 1GB), 2))) +} + +# Export the table to a new CSV +$Table | Sort 'Size (Bytes)' –Descending | Export-CSV –Path $TempLocation\$ExportedFilesCSV –NoTypeInformation –UseCulture + +# Export the largest 100 results into html format +$Table | +Sort 'Size (Bytes)' –Descending | +Select –First 100 | +ConvertTo-Html –Property 'Name', 'Size (Bytes)', 'Size (KB)', 'Size (MB)', 'Size (GB)' –Head $style –Body "

100 largest files on $env:COMPUTERNAME

" –CssUri "http://www.w3schools.com/lib/w3.css" | +Out-String | Out-File $TempLocation\$ExportedFilesHTML + +#endregion + + + +#region Folders + +# Remove the first 2 rows from the CSVs to leave just the relevant data +$CSVContent = Get-Content –Path $TempLocation\$FoldersCSV –ReadCount 0 +$CSVContent = $CSVContent | Select –Skip 1 +$CSVContent = $CSVContent | Select –Skip 1 + +# Create a table to store the results +$Table = [System.Data.DataTable]::new("Directory Structure") +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Name", [System.String])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (Bytes)", [System.Int64])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (KB)", [System.Decimal])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (MB)", [System.Decimal])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Size (GB)", [System.Decimal])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Files", [System.String])) +[void]$Table.Columns.Add([System.Data.DataColumn]::new("Folders", [System.String])) + +# Populate the table from the CSV data +Foreach ($csvrow in $CSVContent) { + $Content = $csvrow.split(',') + [void]$Table.rows.Add($($Content[0].Replace('"', '')), $Content[2], ([math]::Round(($Content[2] / 1KB), 2)), ([math]::Round(($Content[2] / 1MB), 2)), ([math]::Round(($Content[2] / 1GB), 2)), $Content[5], $Content[6]) +} + +# Export the table to a new CSV +$Table | Sort 'Size (Bytes)' –Descending | Export-CSV –Path $TempLocation\$ExportedFoldersCSV –NoTypeInformation –UseCulture + +# Export the largest 100 results into html format +$Table | +Sort 'Size (Bytes)' –Descending | +Select –First 100 | +ConvertTo-Html –Property 'Name', 'Size (Bytes)', 'Size (KB)', 'Size (MB)', 'Size (GB)', 'Files', 'Folders' –Head $style –Body "

100 largest directories on $env:COMPUTERNAME

" –CssUri "http://www.w3schools.com/lib/w3.css" | +Out-String | Out-File $TempLocation\$ExportedFoldersHTML + +#endregion + + + +#region Create HTML disk usage summary report + +# Get system drive data +$WMIDiskInfo = Get-CimInstance –ClassName Win32_Volume –Property Capacity, FreeSpace, DriveLetter | Where { $_.DriveLetter -eq $env:SystemDrive } | Select Capacity, FreeSpace, DriveLetter +$DiskInfo = [pscustomobject]@{ + DriveLetter = $WMIDiskInfo.DriveLetter + 'Capacity (GB)' = [math]::Round(($WMIDiskInfo.Capacity / 1GB), 2) + 'FreeSpace (GB)' = [math]::Round(($WMIDiskInfo.FreeSpace / 1GB), 2) + 'UsedSpace (GB)' = [math]::Round((($WMIDiskInfo.Capacity / 1GB) – ($WMIDiskInfo.FreeSpace / 1GB)), 2) + 'Percent Free' = [math]::Round(($WMIDiskInfo.FreeSpace * 100 / $WMIDiskInfo.Capacity), 2) + 'Percent Used' = [math]::Round((($WMIDiskInfo.Capacity – $WMIDiskInfo.FreeSpace) * 100 / $WMIDiskInfo.Capacity), 2) +} + +# Create html header +$html = @" + + + + +"@ + +# Set html +$html = $html + @" +

Disk Space Usage for Drive $($DiskInfo.DriveLetter) on $env:COMPUTERNAME

+ + + + + +
+ $($DiskInfo.'UsedSpace (GB)') GB ($($DiskInfo.'Percent Used') %) + +
+ + + + + + + + + + +
+ Capacity: $($DiskInfo.'Capacity (GB)') GB +
+ FreeSpace: $($DiskInfo.'FreeSpace (GB)') GB +
+ Percent Free: $($DiskInfo.'Percent Free') % +
+"@ + +If ($DiskInfo.'FreeSpace (GB)' -lt 20) { + + $html = $html + @" + + + + +
+ You need to free $(20 – $DiskInfo.'FreeSpace (GB)') GB on this disk to pass the W10 readiness check! +
+"@ +} + +# Close html document +$html = $html + @" + + +"@ + +# Export to file +$html | +Out-string | +Out-File $TempLocation\$SummaryHTMLReport + + +#endregion + + + + +#region Copy files to share + +# Create a subfolder with computername if doesn't exist +If (!(Test-Path $TargetRoot\$env:COMPUTERNAME)) { + $null = New-Item –Path $TargetRoot –Name $env:COMPUTERNAME –ItemType Directory +} + +# Create a subdirectory with current date-time +$DateString = ((Get-Date).ToUniversalTime() | get-date –Format "yyyy-MM-dd_HH-mm-ss").ToString() +If (!(Test-Path $TargetRoot\$env:COMPUTERNAME\$DateString)) { + $null = New-Item –Path $TargetRoot\$env:COMPUTERNAME –Name $DateString –ItemType Directory +} + +# Set final target location +$TargetLocation = "$TargetRoot\$env:COMPUTERNAME\$DateString" + +# Copy files +$Files = @( + $ExportedFilesCSV + $ExportedFoldersCSV + $ExportedFilesHTML + $ExportedFoldersHTML + $SummaryHTMLReport +) +Try { + Robocopy $TempLocation $TargetLocation $Files /R:10 /W:5 /NP +} +Catch {} + +#endregion + + +# Cleanup temp files +$Files = @( + $FilesCSV + $FoldersCSV + $ExportedFilesCSV + $ExportedFoldersCSV + $ExportedFilesHTML + $ExportedFoldersHTML + $SummaryHTMLReport +) + +Foreach ($file in $files) { + Remove-Item –Path $TempLocation\$file –Force +} + + +# Force a code 0 on exit, in case of some non-terminating error. +ExitWithCode 0 \ No newline at end of file diff --git a/scripts_wip/Win_Disk_Space_Usage_Reports_WiztreeAlt2.ps1 b/scripts_wip/Win_Disk_Space_Usage_Reports_WiztreeAlt2.ps1 new file mode 100644 index 0000000000..a9aa364a93 --- /dev/null +++ b/scripts_wip/Win_Disk_Space_Usage_Reports_WiztreeAlt2.ps1 @@ -0,0 +1,17 @@ +# extract WizTree +Expand-Archive C:\temp\wiztree_3_26_portable.zip -DestinationPath C:\temp\wiztree + +# run wiztree.exe against provided drive/path +# generates diskusage.csv file and uploads to asset, deletes local file after upload + +# If 32-bit +if ([System.IntPtr]::Size -eq 4) { + C:\temp\wiztree\wiztree.exe "$scanpath" /export="c:\temp\wiztree\diskusage.csv" /admin=1 /exportfolders=1 /exportfiles=0 /sortby=2 | Out-Null +} +else { + C:\temp\wiztree\wiztree64.exe "$scanpath" /export="c:\temp\wiztree\diskusage.csv" /admin=1 /exportfolders=1 /exportfiles=0 /sortby=2 | Out-Null +} +# This will upload the file to Syncro and attach it to the Asset. +Upload-File -Subdomain "$subdomain" -FilePath "C:\temp\wiztree\diskusage.csv" +# Delete local file after upload +Remove-Item -Path "C:\temp\wiztree\diskusage.csv" -Force \ No newline at end of file diff --git a/scripts_wip/Win_Powershell_Upgrade.ps1 b/scripts_wip/Win_Powershell_Upgrade.ps1 index ac1519c283..5988f0e21b 100644 --- a/scripts_wip/Win_Powershell_Upgrade.ps1 +++ b/scripts_wip/Win_Powershell_Upgrade.ps1 @@ -5,6 +5,8 @@ # Win 8.1 x64 and Svr 2012 R2 x64 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu # Win 81 x32 https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu +# See https://github.com/wh1te909/tacticalrmm/blob/develop/scripts_wip/Win_Powershell_Version_Check.ps1 for alert script to warn when this is needed + if ($PSVersionTable.PSVersion.Major -lt 5) { Write-Output "Old Version - Need to Upgrade" # Download MSU file - EDIT THIS URL diff --git a/scripts_wip/Win_Powershell_Version_Check.ps1 b/scripts_wip/Win_Powershell_Version_Check.ps1 new file mode 100644 index 0000000000..5f69d64063 --- /dev/null +++ b/scripts_wip/Win_Powershell_Version_Check.ps1 @@ -0,0 +1,12 @@ +# Use as check script for old Powershell version 2.0 (aka Win7) and upgrade using https://github.com/wh1te909/tacticalrmm/blob/develop/scripts_wip/Win_Powershell_Upgrade.ps1 + +if ($PSVersionTable.PSVersion.Major -gt 2) { + $PSVersionTable.PSVersion.Major + Write-Output "PSVersion Greater than 2.0" + exit 0 +} +else { + $PSVersionTable.PSVersion.Major + Write-Output "PSVersion less than 2.0" + exit 1 +} \ No newline at end of file diff --git a/scripts_wip/Win_Speedtest_Ookla.ps1 b/scripts_wip/Win_Speedtest_Ookla.ps1 new file mode 100644 index 0000000000..874721ac59 --- /dev/null +++ b/scripts_wip/Win_Speedtest_Ookla.ps1 @@ -0,0 +1,33 @@ +$runpath = "C:\TechTools\Speedtest\Speedtest.exe" +$zippath = "C:\TechTools\Zip\" +$toolpath = "C:\TechTools\Speedtest\" +$Url = "https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-win64.zip" +$DownloadZipFile = "C:\TechTools\Zip\" + $(Split-Path -Path $Url -Leaf) +$ExtractPath = "C:\TechTools\Speedtest\" + + +#Check for speedtest cli executable, if missing it will check for and create folders required, +#download speedtest cli zip file from $URL and extract into correct folder +IF(!(test-path $runpath)) +{ + #Check for SpeedTest folder, if missing, create + If(!(test-path $toolpath)) + { + New-Item -ItemType Directory -Force -Path $toolpath + } + + #Check for zip folder, if missing, create + If(!(test-path $zippath)) + { + New-Item -ItemType Directory -Force -Path $zippath + } + + #Download and extract zip from the URL in $URL + Invoke-WebRequest -Uri $Url -OutFile $DownloadZipFile + $ExtractShell = New-Object -ComObject Shell.Application + $ExtractFiles = $ExtractShell.Namespace($DownloadZipFile).Items() + $ExtractShell.NameSpace($ExtractPath).CopyHere($ExtractFiles) + +} + +& $runpath \ No newline at end of file diff --git a/scripts_wip/Win_Speedtest_Packetloss.ps1 b/scripts_wip/Win_Speedtest_Packetloss.ps1 new file mode 100644 index 0000000000..65a1fd3546 --- /dev/null +++ b/scripts_wip/Win_Speedtest_Packetloss.ps1 @@ -0,0 +1,65 @@ +Import-Module $env:SyncroModule + +$Random = get-random -min 1 -max 100 +start-sleep $random + +######### Absolute monitoring values ########## +$maxpacketloss = 2 #how much % packetloss until we alert. +$MinimumDownloadSpeed = 10 #What is the minimum expected download speed in Mbit/ps +$MinimumUploadSpeed = 1 #What is the minimum expected upload speed in Mbit/ps +######### End absolute monitoring values ###### + +#Replace the Download URL to where you've uploaded the ZIP file yourself. We will only download this file once. +#Latest version can be found at: https://www.speedtest.net/nl/apps/cli +$DownloadURL = "https://bintray.com/ookla/download/download_file?file_path=ookla-speedtest-1.0.0-win64.zip" +$DownloadLocation = "$($Env:ProgramData)\SpeedtestCLI" +try { + $TestDownloadLocation = Test-Path $DownloadLocation + if (!$TestDownloadLocation) { + new-item $DownloadLocation -ItemType Directory -force + Invoke-WebRequest -Uri $DownloadURL -OutFile "$($DownloadLocation)\speedtest.zip" + Expand-Archive "$($DownloadLocation)\speedtest.zip" -DestinationPath $DownloadLocation -Force + } +} +catch { + write-host "The download and extraction of SpeedtestCLI failed. Error: $($_.Exception.Message)" + exit 1 +} +$PreviousResults = if (test-path "$($DownloadLocation)\LastResults.txt") { get-content "$($DownloadLocation)\LastResults.txt" | ConvertFrom-Json } +$SpeedtestResults = & "$($DownloadLocation)\speedtest.exe" --format=json --accept-license --accept-gdpr +$SpeedtestResults | Out-File "$($DownloadLocation)\LastResults.txt" -Force +$SpeedtestResults = $SpeedtestResults | ConvertFrom-Json + +#creating object +[PSCustomObject]$SpeedtestObj = @{ + downloadspeed = [math]::Round($SpeedtestResults.download.bandwidth / 1000000 * 8, 2) + uploadspeed = [math]::Round($SpeedtestResults.upload.bandwidth / 1000000 * 8, 2) + packetloss = [math]::Round($SpeedtestResults.packetLoss) + isp = $SpeedtestResults.isp + ExternalIP = $SpeedtestResults.interface.externalIp + InternalIP = $SpeedtestResults.interface.internalIp + UsedServer = $SpeedtestResults.server.host + ResultsURL = $SpeedtestResults.result.url + Jitter = [math]::Round($SpeedtestResults.ping.jitter) + Latency = [math]::Round($SpeedtestResults.ping.latency) +} +$SpeedtestHealth = @() +#Comparing against previous result. Alerting is download or upload differs more than 20%. +if ($PreviousResults) { + if ($PreviousResults.download.bandwidth / $SpeedtestResults.download.bandwidth * 100 -le 80) { $SpeedtestHealth += "Download speed difference is more than 20%" } + if ($PreviousResults.upload.bandwidth / $SpeedtestResults.upload.bandwidth * 100 -le 80) { $SpeedtestHealth += "Upload speed difference is more than 20%" } +} + +#Comparing against preset variables. +if ($SpeedtestObj.downloadspeed -lt $MinimumDownloadSpeed) { $SpeedtestHealth += "Download speed is lower than $MinimumDownloadSpeed Mbit/ps" } +if ($SpeedtestObj.uploadspeed -lt $MinimumUploadSpeed) { $SpeedtestHealth += "Upload speed is lower than $MinimumUploadSpeed Mbit/ps" } +if ($SpeedtestObj.packetloss -gt $MaxPacketLoss) { $SpeedtestHealth += "Packetloss is higher than $maxpacketloss%" } + +if (!$SpeedtestHealth) { + $SpeedtestHealth = "Healthy" +} + +Set-Asset-Field -Subdomain "fresh-tech" -Name "Download Speed" -Value $SpeedtestObj.downloadspeed +Set-Asset-Field -Subdomain "fresh-tech" -Name "Upload Speed" -Value $SpeedtestObj.uploadspeed +Set-Asset-Field -Subdomain "fresh-tech" -Name "Packet Loss" -Value $SpeedtestObj.packetloss +Set-Asset-Field -Subdomain "fresh-tech" -Name "Speedtest Health" -Value $SpeedtestHealth From f35fa0aa58dc6b10230a23868cef0beb3d251a82 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Wed, 1 Sep 2021 18:50:18 -0400 Subject: [PATCH 03/33] Troubleshooting docs update --- docs/docs/troubleshooting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index b7c0b29532..8f97d6dc62 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -1,5 +1,16 @@ # Troubleshooting +#### Problems after new install + +In the very unlikely event you have issues after install please wipe the box and install again (following all the steps including downloading the install script but not running it) use the following command which will log the install progress and if you continue to have issues will assist with support of the installation. + +```bash +bash -x install.sh 2>&1 | tee install.logt +``` + +!!!note + Logging of installs isn’t desirable as it logs extremely sensitive information which is why this isn’t done by default! **Do not** post the raw log publicly only provide it if requested and then by dm only. + #### "Bad credentials" error when trying to login to the Web UI If you are sure you are using the correct credentials and still getting a "bad credentials" error, open your browser's dev tools (ctrl + shift + j on chrome) and check the Console tab to see the real error. From 24e4d9cf6de03b0f1ab6c54225f2a85957f2db94 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 05:21:51 -0400 Subject: [PATCH 04/33] docs Making docker howto visible --- docs/mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b484955da9..680a997cd1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -40,8 +40,9 @@ nav: - Tips n' Tricks: tipsntricks.md - Contributing: - "Contributing to Docs": contributing.md - - "Contributing using VSCode": contributing_using_vscode.md - "Contributing to Community Scripts": contributing_community_scripts.md + - "Contributing using VSCode": contributing_using_vscode.md + - "Contributing using Docker": contributing_using_docker.md - License: license.md site_description: "A remote monitoring and management tool" site_author: "wh1te909" From 6ebe1ab4671b9c68c3ae48ecc718738b8cf3b156 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 07:39:44 -0400 Subject: [PATCH 05/33] adding alternate ssl to unsupported docs --- docs/docs/troubleshooting.md | 2 +- docs/docs/unsupported_scripts.md | 364 ++++++++++++++++++++++++++++++- 2 files changed, 364 insertions(+), 2 deletions(-) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 8f97d6dc62..af215d4861 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -5,7 +5,7 @@ In the very unlikely event you have issues after install please wipe the box and install again (following all the steps including downloading the install script but not running it) use the following command which will log the install progress and if you continue to have issues will assist with support of the installation. ```bash -bash -x install.sh 2>&1 | tee install.logt +bash -x install.sh 2>&1 | tee install.log ``` !!!note diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index 9e80a6790d..ef72804374 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -1,4 +1,4 @@ -# Unsupported Reference scripts +# Unsupported Reference Scripts !!!note These are not supported scripts/configurations by Tactical RMM, but it's provided here for your reference. @@ -164,4 +164,366 @@ sudo echo "${tacticalfail2banjail}" > /etc/fail2ban/jail.d/tacticalrmm.local ```bash sudo systemctl restart fail2ban +``` + +## Using purchased SSL certs instead of LetsEncrypt wildcards + +Credit to [@dinger1986](https://github.com/dinger1986) + +How to change certs used by Tactical RMM to purchased ones (this can be a wildcard cert). + +You need to add the certificate private key and public keys to the following files: + +`/etc/nginx/sites-available/rmm.conf` + +`/etc/nginx/sites-available/meshcentral.conf` + +`/etc/nginx/sites-available/frontend.conf` + +`/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` + +1. create a new folder for certs and allow tactical user permissions (assumed to be tactical) + + sudo mkdir /certs + sudo chown -R tactical:tactical /certs" + +2. Now move your certs into that folder. + +3. Open the api file and add the api certificate or if its a wildcard the directory should be `/certs/yourdomain.com/` + + sudo nano /etc/nginx/sites-available/rmm.conf + + replace + + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + + with + + ssl_certificate /certs/api.yourdomain.com/fullchain.pem; + ssl_certificate_key /certs/api.yourdomain.com/privkey.pem; + +4. Repeat the process for + + /etc/nginx/sites-available/meshcentral.conf + /etc/nginx/sites-available/frontend.conf + + but change api. to: mesh. and rmm. respectively. + +7. Add the following to the last lines of `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` + + nano /rmm/api/tacticalrmm/tacticalrmm/local_settings.py + + add + + CERT_FILE = "/certs/api.yourdomain.com/fullchain.pem" + KEY_FILE = "/certs/api.yourdomain.com/privkey.pem" + + +6. Regenerate Nats Conf + + cd /rmm/api/tacticalrmm + source ../env/bin/activate + python manage.py reload_nats + +7. Restart services + + sudo systemctl restart rmm celery celerybeat nginx nats natsapi + +## Use certbot to do acme challenge over http + +The standard SSL cert process in Tactical uses a [DNS challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) that requires dns txt files to be updated with every run. + +The below script uses [http challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) on the 3 separate ssl certs, one for each subdomain: rmm, api, mesh. They still have the same 3 month expiry. You will need to run this script every 3 months (at a minimum - every 2 months probably best) to renew the ssl certs for Tactical RMM. + +!!!note + Your Tactical RMM server will need to have TCP Port: 80 exposed to the internet + +```bash +#!/bin/bash + +###Set colours same as Tactical RMM install and Update +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +### Ubuntu 20.04 Check + +UBU20=$(grep 20.04 "/etc/"*"release") +if ! [[ $UBU20 ]]; then + echo -ne "\033[0;31mThis script will only work on Ubuntu 20.04\e[0m\n" + exit 1 +fi + +cls() { + printf "\033c" +} + +print_green() { + printf >&2 "${GREEN}%0.s-${NC}" {1..80} + printf >&2 "\n" + printf >&2 "${GREEN}${1}${NC}\n" + printf >&2 "${GREEN}%0.s-${NC}" {1..80} + printf >&2 "\n" +} + +cls + +### Set variables for domains + +while [[ $rmmdomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for the backend (e.g. api.example.com)${NC}: " +read rmmdomain +done + +while [[ $frontenddomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for the frontend (e.g. rmm.example.com)${NC}: " +read frontenddomain +done + +while [[ $meshdomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for meshcentral (e.g. mesh.example.com)${NC}: " +read meshdomain +done + +echo -ne "${YELLOW}Enter the current root domain (e.g. example.com or example.co.uk)${NC}: " +read rootdomain + + +### Setup Certificate Variables +CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem +CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem + +### Make Letsencrypt directories + +sudo mkdir /var/www/letsencrypt +sudo mkdir /var/www/letsencrypt/.mesh +sudo mkdir /var/www/letsencrypt/.rmm +sudo mkdir /var/www/letsencrypt/.api + +### Remove config files for nginx + +sudo rm /etc/nginx/sites-available/rmm.conf +sudo rm /etc/nginx/sites-available/meshcentral.conf +sudo rm /etc/nginx/sites-available/frontend.conf +sudo rm /etc/nginx/sites-enabled/rmm.conf +sudo rm /etc/nginx/sites-enabled/meshcentral.conf +sudo rm /etc/nginx/sites-enabled/frontend.conf + +### Setup tactical nginx config files for letsencrypt + +nginxrmm="$(cat << EOF +server_tokens off; +upstream tacticalrmm { + server unix:////rmm/api/tacticalrmm/tacticalrmm.sock; +} +map \$http_user_agent \$ignore_ua { + "~python-requests.*" 0; + "~go-resty.*" 0; + default 1; +} +server { + listen 80; + server_name ${rmmdomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.api/;} + location / { + return 301 https://\$server_name\$request_uri;} +} +server { + listen 443 ssl; + server_name ${rmmdomain}; + client_max_body_size 300M; + access_log /rmm/api/tacticalrmm/tacticalrmm/private/log/access.log; + error_log /rmm/api/tacticalrmm/tacticalrmm/private/log/error.log; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; + + location /static/ { + root /rmm/api/tacticalrmm; + } + location /private/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /rmm/api/tacticalrmm/tacticalrmm/private/; + } +location ~ ^/ws/ { + proxy_pass http://unix:/rmm/daphne.sock; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; +} + location /saltscripts/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /srv/salt/scripts/userdefined/; + } + location /builtin/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /srv/salt/scripts/; + } + location ~ ^/(natsapi) { + allow 127.0.0.1; + deny all; + uwsgi_pass tacticalrmm; + include /etc/nginx/uwsgi_params; + uwsgi_read_timeout 500s; + uwsgi_ignore_client_abort on; + } + location / { + uwsgi_pass tacticalrmm; + include /etc/nginx/uwsgi_params; + uwsgi_read_timeout 9999s; + uwsgi_ignore_client_abort on; + } +} +EOF +)" +echo "${nginxrmm}" | sudo tee /etc/nginx/sites-available/rmm.conf > /dev/null + + +nginxmesh="$(cat << EOF +server { + listen 80; + server_name ${meshdomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.mesh/;} + location / { + return 301 https://\$server_name\$request_uri;} +} +server { + listen 443 ssl; + proxy_send_timeout 330s; + proxy_read_timeout 330s; + server_name ${meshdomain}; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_session_cache shared:WEBSSL:10m; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + location / { + proxy_pass http://127.0.0.1:4430/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Host \$host:\$server_port; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} +EOF +)" +echo "${nginxmesh}" | sudo tee /etc/nginx/sites-available/meshcentral.conf > /dev/null + + + +nginxfrontend="$(cat << EOF +server { + server_name ${frontenddomain}; + charset utf-8; + location / { + root /var/www/rmm/dist; + try_files \$uri \$uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; + } + error_log /var/log/nginx/frontend-error.log; + access_log /var/log/nginx/frontend-access.log; + listen 443 ssl; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; +} +server { + listen 80; + server_name ${frontenddomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.rmm/;} + location / { + return 301 https://\$host\$request_uri;} +} +EOF +)" +echo "${nginxfrontend}" | sudo tee /etc/nginx/sites-available/frontend.conf > /dev/null + +### Relink nginx config files + +sudo ln -s /etc/nginx/sites-available/rmm.conf /etc/nginx/sites-enabled/rmm.conf +sudo ln -s /etc/nginx/sites-available/meshcentral.conf /etc/nginx/sites-enabled/meshcentral.conf +sudo ln -s /etc/nginx/sites-available/frontend.conf /etc/nginx/sites-enabled/frontend.conf + +### Restart nginx + +sudo systemctl restart nginx + + +### Get letsencrypt Certs + +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.mesh/ -d ${meshdomain} +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.rmm/ -d ${frontenddomain} +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.api/ -d ${rmmdomain} + +### Ensure letsencrypt Permissions are correct +sudo chown ${USER}:${USER} -R /etc/letsencrypt +sudo chmod 775 -R /etc/letsencrypt + +### Set variables for new certs + +CERT_PRIV_KEY_API=/etc/letsencrypt/live/${rmmdomain}/privkey.pem +CERT_PUB_KEY_API=/etc/letsencrypt/live/${rmmdomain}/fullchain.pem +CERT_PRIV_KEY_RMM=/etc/letsencrypt/live/${frontenddomain}/privkey.pem +CERT_PUB_KEY_RMM=/etc/letsencrypt/live/${frontenddomain}/fullchain.pem +CERT_PRIV_KEY_MESH=/etc/letsencrypt/live/${meshdomain}/privkey.pem +CERT_PUB_KEY_MESH=/etc/letsencrypt/live/${meshdomain}/fullchain.pem + +### Replace certs in files + +rmmlocalsettings="$(cat << EOF +CERT_FILE = "${CERT_PUB_KEY_API}" +KEY_FILE = "${CERT_PRIV_KEY_API}" +EOF +)" +echo "${rmmlocalsettings}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py > /dev/null + +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_API}|g" /etc/nginx/sites-available/rmm.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_API}|g" /etc/nginx/sites-available/rmm.conf +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_MESH}|g" /etc/nginx/sites-available/meshcentral.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_MESH}|g" /etc/nginx/sites-available/meshcentral.conf +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_RMM}|g" /etc/nginx/sites-available/frontend.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_RMM}|g" /etc/nginx/sites-available/frontend.conf + +### Remove Wildcard Cert + +rm -r /etc/letsencrypt/live/${rootdomain}/ +rm -r /etc/letsencrypt/archive/${rootdomain}/ +rm /etc/letsencrypt/renewal/${rootdomain}.conf + + +### Regenerate Nats Conf +cd /rmm/api/tacticalrmm +source ../env/bin/activate +python manage.py reload_nats + +### Restart services + +for i in rmm celery celerybeat nginx nats natsapi +do +printf >&2 "${GREEN}Restarting ${i} service...${NC}\n" +sudo systemctl restart ${i} +done + + +###Renew certs can be done by sudo letsencrypt renew (this should automatically be in /etc/cron.d/certbot) ``` \ No newline at end of file From 8af69c42845b612d0e5cb8d4ca58198b6c70813d Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 07:55:33 -0400 Subject: [PATCH 06/33] adding alternate ssl to unsupported docs --- docs/docs/troubleshooting.md | 2 +- docs/docs/unsupported_scripts.md | 364 ++++++++++++++++++++++++++++++- 2 files changed, 364 insertions(+), 2 deletions(-) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 8f97d6dc62..af215d4861 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -5,7 +5,7 @@ In the very unlikely event you have issues after install please wipe the box and install again (following all the steps including downloading the install script but not running it) use the following command which will log the install progress and if you continue to have issues will assist with support of the installation. ```bash -bash -x install.sh 2>&1 | tee install.logt +bash -x install.sh 2>&1 | tee install.log ``` !!!note diff --git a/docs/docs/unsupported_scripts.md b/docs/docs/unsupported_scripts.md index 9e80a6790d..de9cf3cc63 100644 --- a/docs/docs/unsupported_scripts.md +++ b/docs/docs/unsupported_scripts.md @@ -1,4 +1,4 @@ -# Unsupported Reference scripts +# Unsupported Reference Scripts !!!note These are not supported scripts/configurations by Tactical RMM, but it's provided here for your reference. @@ -164,4 +164,366 @@ sudo echo "${tacticalfail2banjail}" > /etc/fail2ban/jail.d/tacticalrmm.local ```bash sudo systemctl restart fail2ban +``` + +## Using purchased SSL certs instead of LetsEncrypt wildcards + +Credit to [@dinger1986](https://github.com/dinger1986) + +How to change certs used by Tactical RMM to purchased ones (this can be a wildcard cert). + +You need to add the certificate private key and public keys to the following files: + +`/etc/nginx/sites-available/rmm.conf` + +`/etc/nginx/sites-available/meshcentral.conf` + +`/etc/nginx/sites-available/frontend.conf` + +`/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` + +1. create a new folder for certs and allow tactical user permissions (assumed to be tactical) + + sudo mkdir /certs + sudo chown -R tactical:tactical /certs" + +2. Now move your certs into that folder. + +3. Open the api file and add the api certificate or if its a wildcard the directory should be `/certs/yourdomain.com/` + + sudo nano /etc/nginx/sites-available/rmm.conf + + replace + + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; + + with + + ssl_certificate /certs/api.yourdomain.com/fullchain.pem; + ssl_certificate_key /certs/api.yourdomain.com/privkey.pem; + +4. Repeat the process for + + /etc/nginx/sites-available/meshcentral.conf + /etc/nginx/sites-available/frontend.conf + + but change api. to: mesh. and rmm. respectively. + +7. Add the following to the last lines of `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` + + nano /rmm/api/tacticalrmm/tacticalrmm/local_settings.py + + add + + CERT_FILE = "/certs/api.yourdomain.com/fullchain.pem" + KEY_FILE = "/certs/api.yourdomain.com/privkey.pem" + + +6. Regenerate Nats Conf + + cd /rmm/api/tacticalrmm + source ../env/bin/activate + python manage.py reload_nats + +7. Restart services + + sudo systemctl restart rmm celery celerybeat nginx nats natsapi + +## Use certbot to do acme challenge over http + +The standard SSL cert process in Tactical uses a [DNS challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) that requires dns txt files to be updated with every run. + +The below script uses [http challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) on the 3 separate ssl certs, one for each subdomain: rmm, api, mesh. They still have the same 3 month expiry. Restart the Tactical RMM server about every 2.5 months (80 days) for auto-renewed certs to become active. + +!!!note + Your Tactical RMM server will need to have TCP Port: 80 exposed to the internet + +```bash +#!/bin/bash + +###Set colours same as Tactical RMM install and Update +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +### Ubuntu 20.04 Check + +UBU20=$(grep 20.04 "/etc/"*"release") +if ! [[ $UBU20 ]]; then + echo -ne "\033[0;31mThis script will only work on Ubuntu 20.04\e[0m\n" + exit 1 +fi + +cls() { + printf "\033c" +} + +print_green() { + printf >&2 "${GREEN}%0.s-${NC}" {1..80} + printf >&2 "\n" + printf >&2 "${GREEN}${1}${NC}\n" + printf >&2 "${GREEN}%0.s-${NC}" {1..80} + printf >&2 "\n" +} + +cls + +### Set variables for domains + +while [[ $rmmdomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for the backend (e.g. api.example.com)${NC}: " +read rmmdomain +done + +while [[ $frontenddomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for the frontend (e.g. rmm.example.com)${NC}: " +read frontenddomain +done + +while [[ $meshdomain != *[.]*[.]* ]] +do +echo -ne "${YELLOW}Enter the subdomain used for meshcentral (e.g. mesh.example.com)${NC}: " +read meshdomain +done + +echo -ne "${YELLOW}Enter the current root domain (e.g. example.com or example.co.uk)${NC}: " +read rootdomain + + +### Setup Certificate Variables +CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem +CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem + +### Make Letsencrypt directories + +sudo mkdir /var/www/letsencrypt +sudo mkdir /var/www/letsencrypt/.mesh +sudo mkdir /var/www/letsencrypt/.rmm +sudo mkdir /var/www/letsencrypt/.api + +### Remove config files for nginx + +sudo rm /etc/nginx/sites-available/rmm.conf +sudo rm /etc/nginx/sites-available/meshcentral.conf +sudo rm /etc/nginx/sites-available/frontend.conf +sudo rm /etc/nginx/sites-enabled/rmm.conf +sudo rm /etc/nginx/sites-enabled/meshcentral.conf +sudo rm /etc/nginx/sites-enabled/frontend.conf + +### Setup tactical nginx config files for letsencrypt + +nginxrmm="$(cat << EOF +server_tokens off; +upstream tacticalrmm { + server unix:////rmm/api/tacticalrmm/tacticalrmm.sock; +} +map \$http_user_agent \$ignore_ua { + "~python-requests.*" 0; + "~go-resty.*" 0; + default 1; +} +server { + listen 80; + server_name ${rmmdomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.api/;} + location / { + return 301 https://\$server_name\$request_uri;} +} +server { + listen 443 ssl; + server_name ${rmmdomain}; + client_max_body_size 300M; + access_log /rmm/api/tacticalrmm/tacticalrmm/private/log/access.log; + error_log /rmm/api/tacticalrmm/tacticalrmm/private/log/error.log; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; + + location /static/ { + root /rmm/api/tacticalrmm; + } + location /private/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /rmm/api/tacticalrmm/tacticalrmm/private/; + } +location ~ ^/ws/ { + proxy_pass http://unix:/rmm/daphne.sock; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; +} + location /saltscripts/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /srv/salt/scripts/userdefined/; + } + location /builtin/ { + internal; + add_header "Access-Control-Allow-Origin" "https://${frontenddomain}"; + alias /srv/salt/scripts/; + } + location ~ ^/(natsapi) { + allow 127.0.0.1; + deny all; + uwsgi_pass tacticalrmm; + include /etc/nginx/uwsgi_params; + uwsgi_read_timeout 500s; + uwsgi_ignore_client_abort on; + } + location / { + uwsgi_pass tacticalrmm; + include /etc/nginx/uwsgi_params; + uwsgi_read_timeout 9999s; + uwsgi_ignore_client_abort on; + } +} +EOF +)" +echo "${nginxrmm}" | sudo tee /etc/nginx/sites-available/rmm.conf > /dev/null + + +nginxmesh="$(cat << EOF +server { + listen 80; + server_name ${meshdomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.mesh/;} + location / { + return 301 https://\$server_name\$request_uri;} +} +server { + listen 443 ssl; + proxy_send_timeout 330s; + proxy_read_timeout 330s; + server_name ${meshdomain}; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_session_cache shared:WEBSSL:10m; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + location / { + proxy_pass http://127.0.0.1:4430/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Host \$host:\$server_port; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} +EOF +)" +echo "${nginxmesh}" | sudo tee /etc/nginx/sites-available/meshcentral.conf > /dev/null + + + +nginxfrontend="$(cat << EOF +server { + server_name ${frontenddomain}; + charset utf-8; + location / { + root /var/www/rmm/dist; + try_files \$uri \$uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + add_header Pragma "no-cache"; + } + error_log /var/log/nginx/frontend-error.log; + access_log /var/log/nginx/frontend-access.log; + listen 443 ssl; + ssl_certificate ${CERT_PUB_KEY}; + ssl_certificate_key ${CERT_PRIV_KEY}; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; +} +server { + listen 80; + server_name ${frontenddomain}; + location /.well-known/acme-challenge/ { + root /var/www/letsencrypt/.rmm/;} + location / { + return 301 https://\$host\$request_uri;} +} +EOF +)" +echo "${nginxfrontend}" | sudo tee /etc/nginx/sites-available/frontend.conf > /dev/null + +### Relink nginx config files + +sudo ln -s /etc/nginx/sites-available/rmm.conf /etc/nginx/sites-enabled/rmm.conf +sudo ln -s /etc/nginx/sites-available/meshcentral.conf /etc/nginx/sites-enabled/meshcentral.conf +sudo ln -s /etc/nginx/sites-available/frontend.conf /etc/nginx/sites-enabled/frontend.conf + +### Restart nginx + +sudo systemctl restart nginx + + +### Get letsencrypt Certs + +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.mesh/ -d ${meshdomain} +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.rmm/ -d ${frontenddomain} +sudo letsencrypt certonly --webroot -w /var/www/letsencrypt/.api/ -d ${rmmdomain} + +### Ensure letsencrypt Permissions are correct +sudo chown ${USER}:${USER} -R /etc/letsencrypt +sudo chmod 775 -R /etc/letsencrypt + +### Set variables for new certs + +CERT_PRIV_KEY_API=/etc/letsencrypt/live/${rmmdomain}/privkey.pem +CERT_PUB_KEY_API=/etc/letsencrypt/live/${rmmdomain}/fullchain.pem +CERT_PRIV_KEY_RMM=/etc/letsencrypt/live/${frontenddomain}/privkey.pem +CERT_PUB_KEY_RMM=/etc/letsencrypt/live/${frontenddomain}/fullchain.pem +CERT_PRIV_KEY_MESH=/etc/letsencrypt/live/${meshdomain}/privkey.pem +CERT_PUB_KEY_MESH=/etc/letsencrypt/live/${meshdomain}/fullchain.pem + +### Replace certs in files + +rmmlocalsettings="$(cat << EOF +CERT_FILE = "${CERT_PUB_KEY_API}" +KEY_FILE = "${CERT_PRIV_KEY_API}" +EOF +)" +echo "${rmmlocalsettings}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py > /dev/null + +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_API}|g" /etc/nginx/sites-available/rmm.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_API}|g" /etc/nginx/sites-available/rmm.conf +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_MESH}|g" /etc/nginx/sites-available/meshcentral.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_MESH}|g" /etc/nginx/sites-available/meshcentral.conf +sudo sed -i "s|${CERT_PRIV_KEY}|${CERT_PRIV_KEY_RMM}|g" /etc/nginx/sites-available/frontend.conf +sudo sed -i "s|${CERT_PUB_KEY}|${CERT_PUB_KEY_RMM}|g" /etc/nginx/sites-available/frontend.conf + +### Remove Wildcard Cert + +rm -r /etc/letsencrypt/live/${rootdomain}/ +rm -r /etc/letsencrypt/archive/${rootdomain}/ +rm /etc/letsencrypt/renewal/${rootdomain}.conf + + +### Regenerate Nats Conf +cd /rmm/api/tacticalrmm +source ../env/bin/activate +python manage.py reload_nats + +### Restart services + +for i in rmm celery celerybeat nginx nats natsapi +do +printf >&2 "${GREEN}Restarting ${i} service...${NC}\n" +sudo systemctl restart ${i} +done + + +###Renew certs can be done by sudo letsencrypt renew (this should automatically be in /etc/cron.d/certbot) ``` \ No newline at end of file From 9724882578fc8b46aa5291d66fb77175c1e9c63b Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 08:23:05 -0400 Subject: [PATCH 07/33] wip script for print check --- scripts_wip/Win_SecCheck_Print_kb5005010.ps1 | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 scripts_wip/Win_SecCheck_Print_kb5005010.ps1 diff --git a/scripts_wip/Win_SecCheck_Print_kb5005010.ps1 b/scripts_wip/Win_SecCheck_Print_kb5005010.ps1 new file mode 100644 index 0000000000..7acf5e8f80 --- /dev/null +++ b/scripts_wip/Win_SecCheck_Print_kb5005010.ps1 @@ -0,0 +1,22 @@ +# Checking for insecure by design print features being enabled +# See https://support.microsoft.com/en-us/topic/kb5005010-restricting-installation-of-new-printer-drivers-after-applying-the-july-6-2021-updates-31b91c02-05bc-4ada-a7ea-183b129578a7 + +$PointAndPrintNoElevation = (Get-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrintNoElevation").NoWarningNoElevationOnInstall +$PointAndPrintUpdatePrompt = (Get-ItemProperty -Path "HKLM:\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrintNoElevation").UpdatePromptSettings + +if ($PointAndPrintNoElevation -Eq 1) { + Write-Output "Point and Print WarningNoElevationOnInstall set to true. WARNING: You are insecure-by-design." + exit 1 +} + +elseif ($PointAndPrintUpdatePrompt -Eq 1) { + Write-Output "Point and Print PointAndPrintUpdatePrompt set to true. WARNING: You are insecure-by-design." + exit 1 +} + +else { + Write-Output "WarningNoElevationOnInstall UpdatePromptSettings set to false. No vulnerabilities" + exit 0 +} + +Exit $LASTEXITCODE \ No newline at end of file From b591f9f5b71c3bb893955681df9348642fdc5c57 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 08:39:03 -0400 Subject: [PATCH 08/33] MOAR wips --- scripts_wip/Win11_Update_StartMenu.bat | 9 ++ .../Win_Hardware_Disk_SMART_PassFail.ps1 | 18 +++ .../Win_Hardware_Disk_SMART_detailed.ps1 | 129 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 scripts_wip/Win11_Update_StartMenu.bat create mode 100644 scripts_wip/Win_Hardware_Disk_SMART_PassFail.ps1 create mode 100644 scripts_wip/Win_Hardware_Disk_SMART_detailed.ps1 diff --git a/scripts_wip/Win11_Update_StartMenu.bat b/scripts_wip/Win11_Update_StartMenu.bat new file mode 100644 index 0000000000..ef73efc895 --- /dev/null +++ b/scripts_wip/Win11_Update_StartMenu.bat @@ -0,0 +1,9 @@ +rem Block Win11 upgrade + +reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /f /v TargetReleaseVersion /t REG_DWORD /d 1 +reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /f /v TargetReleaseVersionInfo /t REG_SZ /d 21H2 + +rem classic start menu and left side settings: + +reg add HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\Start_ShowClassicMode /t REG_DWORD /d 1 +reg add HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarAl /t REG_DWORD /d 0 \ No newline at end of file diff --git a/scripts_wip/Win_Hardware_Disk_SMART_PassFail.ps1 b/scripts_wip/Win_Hardware_Disk_SMART_PassFail.ps1 new file mode 100644 index 0000000000..877ee3bfcc --- /dev/null +++ b/scripts_wip/Win_Hardware_Disk_SMART_PassFail.ps1 @@ -0,0 +1,18 @@ +$ErrorActionPreference= 'silentlycontinue' +$smartst = (Get-WmiObject -namespace root\wmi -class MSStorageDriver_FailurePredictStatus).PredictFailure + +if ($smartst = 'False') +{ +Write-Output "Theres no SMART Failures predicted" +exit 0 +} + + +else +{ +Write-Output "There are SMART Failures detected" +exit 1 +} + + +Exit $LASTEXITCODE \ No newline at end of file diff --git a/scripts_wip/Win_Hardware_Disk_SMART_detailed.ps1 b/scripts_wip/Win_Hardware_Disk_SMART_detailed.ps1 new file mode 100644 index 0000000000..85b3eef6d2 --- /dev/null +++ b/scripts_wip/Win_Hardware_Disk_SMART_detailed.ps1 @@ -0,0 +1,129 @@ +# If this is a virtual machine, we don't need to continue +$Computer = Get-CimInstance -ClassName 'Win32_ComputerSystem' +if ($Computer.Model -like 'Virtual*') { + exit +} + +$disks = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | + Select-Object 'InstanceName') + +$Warnings = @() + +foreach ($disk in $disks.InstanceName) { + # Retrieve SMART data + $SmartData = (Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_ATAPISMartData' | + Where-Object 'InstanceName' -eq $disk) + + [Byte[]]$RawSmartData = $SmartData | Select-Object -ExpandProperty 'VendorSpecific' + + # Starting at the third number (first two are irrelevant) + # get the relevant data by iterating over every 12th number + # and saving the values from an offset of the SMART attribute ID + [PSCustomObject[]]$Output = for ($i = 2; $i -lt $RawSmartData.Count; $i++) { + if (0 -eq ($i - 2) % 12 -and $RawSmartData[$i] -ne 0) { + # Construct the raw attribute value by combining the two bytes that make it up + [Decimal]$RawValue = ($RawSmartData[$i + 6] * [Math]::Pow(2, 8) + $RawSmartData[$i + 5]) + + $InnerOutput = [PSCustomObject]@{ + DiskID = $disk + ID = $RawSmartData[$i] + #Flags = $RawSmartData[$i + 1] + #Value = $RawSmartData[$i + 3] + Worst = $RawSmartData[$i + 4] + RawValue = $RawValue + } + + $InnerOutput + } + } + + # Reallocated Sectors Count + $Warnings += $Output | Where-Object ID -eq 5 | Where-Object RawValue -gt 1 | Format-Table + + # Spin Retry Count + $Warnings += $Output | Where-Object ID -eq 10 | Where-Object RawValue -ne 0 | Format-Table + + # Recalibration Retries + $Warnings += $Output | Where-Object ID -eq 11 | Where-Object RawValue -ne 0 | Format-Table + + # Used Reserved Block Count Total + $Warnings += $Output | Where-Object ID -eq 179 | Where-Object RawValue -gt 1 | Format-Table + + # Erase Failure Count + $Warnings += $Output | Where-Object ID -eq 182 | Where-Object RawValue -ne 0 | Format-Table + + # SATA Downshift Error Count or Runtime Bad Block + $Warnings += $Output | Where-Object ID -eq 183 | Where-Object RawValue -ne 0 | Format-Table + + # End-to-End error / IOEDC + $Warnings += $Output | Where-Object ID -eq 184 | Where-Object RawValue -ne 0 | Format-Table + + # Reported Uncorrectable Errors + $Warnings += $Output | Where-Object ID -eq 187 | Where-Object RawValue -ne 0 | Format-Table + + # Command Timeout + $Warnings += $Output | Where-Object ID -eq 188 | Where-Object RawValue -gt 2 | Format-Table + + # High Fly Writes + $Warnings += $Output | Where-Object ID -eq 189 | Where-Object RawValue -ne 0 | Format-Table + + # Temperature Celcius + $Warnings += $Output | Where-Object ID -eq 194 | Where-Object RawValue -gt 50 | Format-Table + + # Reallocation Event Count + $Warnings += $Output | Where-Object ID -eq 196 | Where-Object RawValue -ne 0 | Format-Table + + # Current Pending Sector Count + $Warnings += $Output | Where-Object ID -eq 197 | Where-Object RawValue -ne 0 | Format-Table + + # Uncorrectable Sector Count + $Warnings += $Output | Where-Object ID -eq 198 | Where-Object RawValue -ne 0 | Format-Table + + # UltraDMA CRC Error Count + $Warnings += $Output | Where-Object ID -eq 199 | Where-Object RawValue -ne 0 | Format-Table + + # Soft Read Error Rate + $Warnings += $Output | Where-Object ID -eq 201 | Where-Object Worst -lt 95 | Format-Table + + # SSD Life Left + $Warnings += $Output | Where-Object ID -eq 231 | Where-Object Worst -lt 50 | Format-Table + + # SSD Media Wear Out Indicator + $Warnings += $Output | Where-Object ID -eq 233 | Where-Object Worst -lt 50 | Format-Table + +} + +$Warnings += Get-CimInstance -Namespace 'Root\WMI' -ClassName 'MSStorageDriver_FailurePredictStatus' | + Select-Object InstanceName, PredictFailure, Reason | + Where-Object {$_.PredictFailure -ne $False} | Format-Table + +$Warnings += Get-CimInstance -ClassName 'Win32_DiskDrive' | + Select-Object Model, SerialNumber, Name, Size, Status | + Where-Object {$_.status -ne 'OK'} | Format-Table + +$Warnings += Get-PhysicalDisk | + Select-Object FriendlyName, Size, MediaType, OperationalStatus, HealthStatus | + Where-Object {$_.OperationalStatus -ne 'OK' -or $_.HealthStatus -ne 'Healthy'} | Format-Table + +if ($Warnings) { + $Warnings = $warnings | Out-String + $Warnings + Write-Output "There are SMART impending Failures" + Write-Output "$Warnings" + Exit 2 +} + +elseif ($Error) { + Write-Output "There were errors detecting smart on this system" + Write-Output "$Error" + exit 1 +} + +else +{ +Write-Output "There are no SMART Failures detected" +exit 0 +} + + +Exit $LASTEXITCODE \ No newline at end of file From 1c5e736dcefaf3760c611831b2ba27a031bf39f7 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 10:33:25 -0400 Subject: [PATCH 09/33] wip script network scanner --- scripts_wip/Win_Network_Scan.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 scripts_wip/Win_Network_Scan.ps1 diff --git a/scripts_wip/Win_Network_Scan.ps1 b/scripts_wip/Win_Network_Scan.ps1 new file mode 100644 index 0000000000..7060e5ac63 --- /dev/null +++ b/scripts_wip/Win_Network_Scan.ps1 @@ -0,0 +1,18 @@ +# https://github.com/knk90 +# Installs Angry IP scanner using choco, runs a scan of the network and then uninstalls it + +choco.exe install angryip -y +$PSDefaultParameterValues['*:Encoding'] = 'ascii' +$ips = get-netipaddress -AddressFamily ipv4 | select-object ipaddress +foreach ($i in $ips) { + $split = $i.ipaddress.Split(".") + $startrange = $split[0] + "." + $split[1] + "." + $split[2] + "." + "1" + $endrange = $split[0] + "." + $split[1] + "." + $split[2] + "." + "254" + $command = "`"c:\Program Files\Angry IP Scanner\ipscan.exe`" -f:range " + $startrange + " " + $endrange + " -s -q -o c:\programdata\ipscanoutput.txt`"" + if ($startrange -notlike "*127.0*") { + $command | Out-file -Encoding ASCII c:\programdata\ipscan.bat + c:\programdata\ipscan.bat + type c:\programdata\ipscanoutput.txt + } +} +choco.exe uninstall angryip -y \ No newline at end of file From 926ed55b9bff8970b15f657e6e0589257c5d5a54 Mon Sep 17 00:00:00 2001 From: silversword411 Date: Thu, 2 Sep 2021 11:28:05 -0400 Subject: [PATCH 10/33] docs update - Authorized users --- docs/docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index af215d4861..f4fdad5a9c 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -9,7 +9,7 @@ bash -x install.sh 2>&1 | tee install.log ``` !!!note - Logging of installs isn’t desirable as it logs extremely sensitive information which is why this isn’t done by default! **Do not** post the raw log publicly only provide it if requested and then by dm only. + Logging of installs isn’t desirable as it logs extremely sensitive information which is why this isn’t done by default! **Do not** post the raw log publicly only provide it if requested and then by dm only. Authorized users in Discord are: @BurningTimes#1938 @sadnub#6992 @dinger1986#1734 @silversword#9652 #### "Bad credentials" error when trying to login to the Web UI From 4321affddb3382d1842eebfca34d08cd0b9bfed8 Mon Sep 17 00:00:00 2001 From: sadnub Date: Thu, 2 Sep 2021 21:10:17 -0400 Subject: [PATCH 11/33] allow for creating special tokens for api access and bypassing two factor auth --- .../migrations/0026_auto_20210901_1247.py | 34 +++ .../migrations/0027_auto_20210903_0054.py | 25 +++ api/tacticalrmm/accounts/models.py | 21 ++ api/tacticalrmm/accounts/permissions.py | 5 + api/tacticalrmm/accounts/serializers.py | 18 +- api/tacticalrmm/accounts/urls.py | 2 + api/tacticalrmm/accounts/views.py | 44 +++- api/tacticalrmm/tacticalrmm/auth.py | 64 ++++++ api/tacticalrmm/tacticalrmm/permissions.py | 3 + web/src/api/accounts.js | 27 ++- web/src/components/core/APIKeysForm.vue | 122 ++++++++++ web/src/components/core/APIKeysTable.vue | 212 ++++++++++++++++++ web/src/components/modals/admin/RolesForm.vue | 2 + web/src/components/modals/admin/UserForm.vue | 12 +- .../modals/coresettings/EditCoreSettings.vue | 7 + web/src/composables/accounts.js | 8 +- 16 files changed, 598 insertions(+), 8 deletions(-) create mode 100644 api/tacticalrmm/accounts/migrations/0026_auto_20210901_1247.py create mode 100644 api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py create mode 100644 api/tacticalrmm/tacticalrmm/auth.py create mode 100644 web/src/components/core/APIKeysForm.vue create mode 100644 web/src/components/core/APIKeysTable.vue diff --git a/api/tacticalrmm/accounts/migrations/0026_auto_20210901_1247.py b/api/tacticalrmm/accounts/migrations/0026_auto_20210901_1247.py new file mode 100644 index 0000000000..2e9c3f0e1d --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0026_auto_20210901_1247.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.6 on 2021-09-01 12:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0025_auto_20210721_0424'), + ] + + operations = [ + migrations.CreateModel( + name='APIKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_by', models.CharField(blank=True, max_length=100, null=True)), + ('created_time', models.DateTimeField(auto_now_add=True, null=True)), + ('modified_by', models.CharField(blank=True, max_length=100, null=True)), + ('modified_time', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=25, unique=True)), + ('key', models.CharField(blank=True, max_length=48, unique=True)), + ('expiration', models.DateTimeField(blank=True, default=None, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='role', + name='can_manage_api_keys', + field=models.BooleanField(default=False), + ), + ] diff --git a/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py b/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py new file mode 100644 index 0000000000..684cb63cf0 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0027_auto_20210903_0054.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.6 on 2021-09-03 00:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0026_auto_20210901_1247'), + ] + + operations = [ + migrations.AddField( + model_name='apikey', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='block_dashboard_login', + field=models.BooleanField(default=False), + ), + ] diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index 74287951af..1bb5cab4cc 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from django.db.models.fields import CharField, DateTimeField from logs.models import BaseAuditModel @@ -24,6 +25,7 @@ class User(AbstractUser, BaseAuditModel): is_active = models.BooleanField(default=True) + block_dashboard_login = models.BooleanField(default=False) totp_key = models.CharField(max_length=50, null=True, blank=True) dark_mode = models.BooleanField(default=True) show_community_scripts = models.BooleanField(default=True) @@ -138,6 +140,9 @@ class Role(BaseAuditModel): can_manage_accounts = models.BooleanField(default=False) can_manage_roles = models.BooleanField(default=False) + # authentication + can_manage_api_keys = models.BooleanField(default=False) + def __str__(self): return self.name @@ -186,4 +191,20 @@ def perms(): "can_manage_winupdates", "can_manage_accounts", "can_manage_roles", + "can_manage_api_keys" ] + +class APIKey(BaseAuditModel): + name = CharField(unique=True, max_length=25) + key = CharField(unique=True, blank=True, max_length=48) + expiration = DateTimeField(blank=True, null=True, default=None) + user = models.ForeignKey( + "accounts.User", + related_name="api_key", + on_delete=models.CASCADE, + ) + + @staticmethod + def serialize(apikey): + from .serializers import APIKeyAuditSerializer + return APIKeyAuditSerializer(apikey).data \ No newline at end of file diff --git a/api/tacticalrmm/accounts/permissions.py b/api/tacticalrmm/accounts/permissions.py index e6d6fa5f1a..1a99103939 100644 --- a/api/tacticalrmm/accounts/permissions.py +++ b/api/tacticalrmm/accounts/permissions.py @@ -17,3 +17,8 @@ def has_permission(self, r, view): return True return _has_perm(r, "can_manage_roles") + +class APIKeyPerms(permissions.BasePermission): + def has_permission(self, r, view): + + return _has_perm(r, "can_manage_api_keys") diff --git a/api/tacticalrmm/accounts/serializers.py b/api/tacticalrmm/accounts/serializers.py index 6847720856..93eabbb00a 100644 --- a/api/tacticalrmm/accounts/serializers.py +++ b/api/tacticalrmm/accounts/serializers.py @@ -1,7 +1,7 @@ import pyotp -from rest_framework.serializers import ModelSerializer, SerializerMethodField +from rest_framework.serializers import ModelSerializer, SerializerMethodField, ReadOnlyField -from .models import User, Role +from .models import APIKey, User, Role class UserUISerializer(ModelSerializer): @@ -64,3 +64,17 @@ class RoleAuditSerializer(ModelSerializer): class Meta: model = Role fields = "__all__" + +class APIKeySerializer(ModelSerializer): + class Meta: + model = APIKey + fields = "__all__" +class APIKeyAuditSerializer(ModelSerializer): + + user = ReadOnlyField(source="user.username") + class Meta: + model = APIKey + fields = [ + "name", + "expiration" + ] \ No newline at end of file diff --git a/api/tacticalrmm/accounts/urls.py b/api/tacticalrmm/accounts/urls.py index bd03753c13..a8356869d2 100644 --- a/api/tacticalrmm/accounts/urls.py +++ b/api/tacticalrmm/accounts/urls.py @@ -12,4 +12,6 @@ path("permslist/", views.PermsList.as_view()), path("roles/", views.GetAddRoles.as_view()), path("/role/", views.GetUpdateDeleteRole.as_view()), + path("apikeys/", views.GetAddAPIKeys.as_view()), + path("apikeys//", views.GetUpdateDeleteAPIKey.as_view()), ] diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index a79017b896..89e1fed9c7 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -13,9 +13,10 @@ from rest_framework.views import APIView from tacticalrmm.utils import notify_error -from .models import Role, User -from .permissions import AccountsPerms, RolesPerms +from .models import APIKey, Role, User +from .permissions import APIKeyPerms, AccountsPerms, RolesPerms from .serializers import ( + APIKeySerializer, RoleSerializer, TOTPSetupSerializer, UserSerializer, @@ -252,3 +253,42 @@ def delete(self, request, pk): role = get_object_or_404(Role, pk=pk) role.delete() return Response("ok") + +class GetAddAPIKeys(APIView): + permission_classes = [IsAuthenticated, APIKeyPerms] + + def get(self, request): + apikeys = APIKey.objects.all() + return Response(APIKeySerializer(apikeys, many=True).data) + + def post(self, request): + # generate a random API Key + # https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits/23728630#23728630 + import random + import string + request.data["key"] = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)) + + serializer = APIKeySerializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj = serializer.save() + return Response("The API Key was added") + +class GetUpdateDeleteAPIKey(APIView): + permission_classes = [IsAuthenticated, APIKeyPerms] + + def put(self, request, pk): + apikey = get_object_or_404(APIKey, pk=pk) + + # remove API key is present in request data + if "key" in request.data.keys(): + request.data.pop("key") + + serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response("The API Key was edited") + + def delete(self, request, pk): + apikey = get_object_or_404(APIKey, pk=pk) + apikey.delete() + return Response("The API Key was deleted") \ No newline at end of file diff --git a/api/tacticalrmm/tacticalrmm/auth.py b/api/tacticalrmm/tacticalrmm/auth.py new file mode 100644 index 0000000000..153e5fd563 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/auth.py @@ -0,0 +1,64 @@ + +from django.utils import timezone as djangotime +from django.utils.translation import ugettext_lazy as _ +from rest_framework import exceptions +from rest_framework.authentication import ( + BaseAuthentication, HTTP_HEADER_ENCODING +) + +from accounts.models import APIKey + +def get_authorization_header(request): + """ + Return request's 'Authorization:' header, as a bytestring. + + Hide some test client ickyness where the header can be unicode. + """ + auth = request.META.get('HTTP_X_API_KEY', b'') + if isinstance(auth, str): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + return auth + + +class APIAuthentication(BaseAuthentication): + """ + Simple token based authentication for stateless api access. + + Clients should authenticate by passing the token key in the "X-API-KEY" + HTTP header. For example: + + X-API-KEY: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 + """ + + def get_model(self): + return APIKey + + def authenticate(self, request): + auth = get_authorization_header(request) + + if not auth: + return None + + try: + apikey = auth.decode() + except UnicodeError: + msg = _('Invalid token header. Token string should not contain invalid characters.') + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(apikey) + + def authenticate_credentials(self, key): + try: + apikey = APIKey.objects.select_related('user').get(key=key) + except APIKey.DoesNotExist: + raise exceptions.AuthenticationFailed(_('Invalid token.')) + + if not apikey.user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + # check if token is expired + if apikey.expiration and apikey.expiration < djangotime.now(): + raise exceptions.AuthenticationFailed(_('The token as expired.')) + + return (apikey.user, apikey.key) diff --git a/api/tacticalrmm/tacticalrmm/permissions.py b/api/tacticalrmm/tacticalrmm/permissions.py index ccd46abbe8..f5310af295 100644 --- a/api/tacticalrmm/tacticalrmm/permissions.py +++ b/api/tacticalrmm/tacticalrmm/permissions.py @@ -1,3 +1,6 @@ +from rest_framework import permissions +from tacticalrmm.auth import APIAuthentication + def _has_perm(request, perm): if request.user.is_superuser or ( request.user.role and getattr(request.user.role, "is_superuser") diff --git a/web/src/api/accounts.js b/web/src/api/accounts.js index dcb4b3b3e8..1dc01fc358 100644 --- a/web/src/api/accounts.js +++ b/web/src/api/accounts.js @@ -2,9 +2,34 @@ import axios from "axios" const baseUrl = "/accounts" +// user api functions export async function fetchUsers(params = {}) { try { const { data } = await axios.get(`${baseUrl}/users/`, { params: params }) return data } catch (e) { } -} \ No newline at end of file +} + + +// api key api functions +export async function fetchAPIKeys(params = {}) { + try { + const { data } = await axios.get(`${baseUrl}/apikeys/`, { params: params }) + return data + } catch (e) { } +} + +export async function saveAPIKey(payload) { + const { data } = await axios.post(`${baseUrl}/apikeys/`, payload) + return data +} + +export async function editAPIKey(payload) { + const { data } = await axios.put(`${baseUrl}/apikeys/${payload.id}/`, payload) + return data +} + +export async function removeAPIKey(id) { + const { data } = await axios.delete(`${baseUrl}/apikeys/${id}/`) + return data +} diff --git a/web/src/components/core/APIKeysForm.vue b/web/src/components/core/APIKeysForm.vue new file mode 100644 index 0000000000..90cc8e9677 --- /dev/null +++ b/web/src/components/core/APIKeysForm.vue @@ -0,0 +1,122 @@ + + + \ No newline at end of file diff --git a/web/src/components/core/APIKeysTable.vue b/web/src/components/core/APIKeysTable.vue new file mode 100644 index 0000000000..87a356f0c3 --- /dev/null +++ b/web/src/components/core/APIKeysTable.vue @@ -0,0 +1,212 @@ + + + \ No newline at end of file diff --git a/web/src/components/modals/admin/RolesForm.vue b/web/src/components/modals/admin/RolesForm.vue index 8455a357dd..5f3a9b4841 100644 --- a/web/src/components/modals/admin/RolesForm.vue +++ b/web/src/components/modals/admin/RolesForm.vue @@ -56,6 +56,7 @@ + @@ -180,6 +181,7 @@ export default { can_manage_notes: false, can_view_core_settings: false, can_edit_core_settings: false, + can_manage_api_keys: false, can_do_server_maint: false, can_code_sign: false, can_manage_checks: false, diff --git a/web/src/components/modals/admin/UserForm.vue b/web/src/components/modals/admin/UserForm.vue index d06a2b233c..091e4670e9 100644 --- a/web/src/components/modals/admin/UserForm.vue +++ b/web/src/components/modals/admin/UserForm.vue @@ -68,7 +68,7 @@
Active:
- +
@@ -88,6 +88,14 @@ class="col-10" /> + + + @@ -109,6 +117,7 @@ export default { return { localUser: { is_active: true, + deny_dashboard_login: false, }, roles: [], isPwd: true, @@ -146,6 +155,7 @@ export default { // dont allow updating is_active if username is same as logged in user if (this.localUser.username === this.logged_in_user) { delete this.localUser.is_active; + delete this.localUser.deny_dashboard_login; } this.$axios diff --git a/web/src/components/modals/coresettings/EditCoreSettings.vue b/web/src/components/modals/coresettings/EditCoreSettings.vue index 41dcde7ef0..0781b06088 100644 --- a/web/src/components/modals/coresettings/EditCoreSettings.vue +++ b/web/src/components/modals/coresettings/EditCoreSettings.vue @@ -11,6 +11,7 @@ +