Compare commits

..

1 Commits

Author SHA1 Message Date
Manfred Touron
2d4a99b127 chore: switch to multiline function definition in resolver.go 2018-12-09 11:37:36 +01:00
54 changed files with 1202 additions and 2218 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,12 +1,12 @@
digraph {
node[shape=record;style=rounded;fontname="helvetica-bold"];
graph[layout=dot;rankdir=LR;overlap=prism;splines=true;fontname="helvetica-bold"];
edge[arrowhead=none;fontname="helvetica"];
start[label="\$\> ssh sshportal";color=blue;fontcolor=blue;fontsize=18];
rankdir=LR;
layout=dot;
node[shape=record];
start[label="ssh sshportal";color=blue;fontcolor=blue;fontsize=20];
subgraph cluster_sshportal {
graph[fontsize=18;color=gray;fontcolor=black];
graph[fontsize=20;style=dashed;color=purple;fontcolor=purple];
label="sshportal";
{
node[color=darkorange;fontcolor=darkorange];
@@ -17,25 +17,25 @@ digraph {
}
{
node[color=darkgreen;fontcolor=darkgreen];
builtin_shell[label="built-in\nconfig shell"];
ssh_proxy[label="SSH proxy\nJump-Host"];
learn_key[label="learn the\npub key"];
builtin_shell[label="built-in shell"];
ssh_proxy[label="SSH proxy"];
learn_key[label="learn key"];
}
err_and_exit[label="\nerror\nand exit\n\n";color=red;fontcolor=red];
err_and_exit[label="error and exit";color=red;fontcolor=red];
{ rank=same; ssh_proxy; builtin_shell; learn_key; err_and_exit; }
{ rank=same; known_user_key; unknown_user_key; }
}
subgraph cluster_hosts {
label="your hosts";
graph[fontsize=18;color=gray;fontcolor=black];
graph[fontsize=20;style=dashed;color=purple;fontcolor=purple];
node[color=blue;fontcolor=blue];
host_1[label="root@host1"];
host_2[label="user@host2:2222"];
host_3[label="root@host3:1234"];
}
{
edge[color=blue];
start -> known_user_key;
@@ -53,13 +53,13 @@ digraph {
{
edge[color=darkorange;fontcolor=darkorange];
known_user_key -> acl_manager[label="user matches an existing host"];
unknown_user_key -> invite_manager[label="user=invite:<token>";labelloc=b];
unknown_user_key -> invite_manager[headlabel="user=invite:<token>"];
}
{
edge[color=red;fontcolor=red];
known_user_key -> err_and_exit[label="invalid user"];
acl_manager -> err_and_exit[label="unauthorized"];
unknown_user_key -> err_and_exit[label="any other user";constraint=false];
invite_manager -> err_and_exit[label="invalid token";constraint=false];
unknown_user_key -> err_and_exit[label="any other user"];
invite_manager -> err_and_exit[label="invalid token"];
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -4,175 +4,185 @@
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: %3 Pages: 1 -->
<svg width="1150pt" height="310pt"
viewBox="0.00 0.00 1149.83 310.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 306)">
<svg width="1026pt" height="312pt"
viewBox="0.00 0.00 1026.42 312.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 308)">
<title>%3</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-306 1145.8281,-306 1145.8281,4 -4,4"/>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-308 1022.4219,-308 1022.4219,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_sshportal</title>
<polygon fill="none" stroke="#c0c0c0" points="187.5586,-8 187.5586,-294 964.46,-294 964.46,-8 187.5586,-8"/>
<text text-anchor="middle" x="576.0093" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">sshportal</text>
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="147.7832,-8 147.7832,-296 858.9775,-296 858.9775,-8 147.7832,-8"/>
<text text-anchor="middle" x="503.3804" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">sshportal</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_hosts</title>
<polygon fill="none" stroke="#c0c0c0" points="985.46,-104 985.46,-294 1133.8281,-294 1133.8281,-104 985.46,-104"/>
<text text-anchor="middle" x="1059.644" y="-275.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#000000">your hosts</text>
<polygon fill="none" stroke="#a020f0" stroke-dasharray="5,2" points="879.9775,-104 879.9775,-296 1010.4219,-296 1010.4219,-104 879.9775,-104"/>
<text text-anchor="middle" x="945.1997" y="-276" font-family="Times,serif" font-size="20.00" fill="#a020f0">your hosts</text>
</g>
<!-- start -->
<g id="node1" class="node">
<title>start</title>
<path fill="none" stroke="#0000ff" d="M12,-122C12,-122 146.5586,-122 146.5586,-122 152.5586,-122 158.5586,-128 158.5586,-134 158.5586,-134 158.5586,-146 158.5586,-146 158.5586,-152 152.5586,-158 146.5586,-158 146.5586,-158 12,-158 12,-158 6,-158 0,-152 0,-146 0,-146 0,-134 0,-134 0,-128 6,-122 12,-122"/>
<text text-anchor="middle" x="79.2793" y="-134.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="18.00" fill="#0000ff">$&gt; ssh sshportal</text>
<polygon fill="none" stroke="#0000ff" points="0,-118 0,-154 118.7832,-154 118.7832,-118 0,-118"/>
<text text-anchor="middle" x="59.3916" y="-130" font-family="Times,serif" font-size="20.00" fill="#0000ff">ssh sshportal</text>
</g>
<!-- known_user_key -->
<g id="node2" class="node">
<title>known_user_key</title>
<path fill="none" stroke="#ff8c00" d="M216.1104,-161C216.1104,-161 313.1514,-161 313.1514,-161 319.1514,-161 325.1514,-167 325.1514,-173 325.1514,-173 325.1514,-185 325.1514,-185 325.1514,-191 319.1514,-197 313.1514,-197 313.1514,-197 216.1104,-197 216.1104,-197 210.1104,-197 204.1104,-191 204.1104,-185 204.1104,-185 204.1104,-173 204.1104,-173 204.1104,-167 210.1104,-161 216.1104,-161"/>
<text text-anchor="middle" x="264.6309" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">known user key</text>
<polygon fill="none" stroke="#ff8c00" points="162.7832,-157 162.7832,-193 267.4316,-193 267.4316,-157 162.7832,-157"/>
<text text-anchor="middle" x="215.1074" y="-170.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">known user key</text>
</g>
<!-- start&#45;&gt;known_user_key -->
<g id="edge1" class="edge">
<title>start&#45;&gt;known_user_key</title>
<path fill="none" stroke="#0000ff" d="M158.6917,-156.7092C173.8232,-159.8931 189.4365,-163.1783 203.8727,-166.2158"/>
<path fill="none" stroke="#0000ff" d="M119.1501,-150.9669C130.1162,-153.7134 141.5894,-156.587 152.6326,-159.3528"/>
<polygon fill="#0000ff" stroke="#0000ff" points="152.0758,-162.8214 162.6266,-161.8558 153.7765,-156.0311 152.0758,-162.8214"/>
</g>
<!-- unknown_user_key -->
<g id="node3" class="node">
<title>unknown_user_key</title>
<path fill="none" stroke="#ff8c00" d="M207.5586,-69C207.5586,-69 321.7031,-69 321.7031,-69 327.7031,-69 333.7031,-75 333.7031,-81 333.7031,-81 333.7031,-93 333.7031,-93 333.7031,-99 327.7031,-105 321.7031,-105 321.7031,-105 207.5586,-105 207.5586,-105 201.5586,-105 195.5586,-99 195.5586,-93 195.5586,-93 195.5586,-81 195.5586,-81 195.5586,-75 201.5586,-69 207.5586,-69"/>
<text text-anchor="middle" x="264.6309" y="-82.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">unknown user key</text>
<polygon fill="none" stroke="#ff8c00" points="155.7832,-72 155.7832,-108 274.4316,-108 274.4316,-72 155.7832,-72"/>
<text text-anchor="middle" x="215.1074" y="-85.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">unknown user key</text>
</g>
<!-- start&#45;&gt;unknown_user_key -->
<g id="edge2" class="edge">
<title>start&#45;&gt;unknown_user_key</title>
<path fill="none" stroke="#0000ff" d="M142.2895,-121.9827C161.3902,-116.521 182.3703,-110.5218 201.4801,-105.0575"/>
<path fill="none" stroke="#0000ff" d="M119.1501,-118.3468C127.968,-115.7419 137.1138,-113.0401 146.1003,-110.3854"/>
<polygon fill="#0000ff" stroke="#0000ff" points="147.1673,-113.7198 155.766,-107.5301 145.1841,-107.0066 147.1673,-113.7198"/>
</g>
<!-- acl_manager -->
<g id="node5" class="node">
<title>acl_manager</title>
<path fill="none" stroke="#ff8c00" d="M608.9287,-173C608.9287,-173 691.7031,-173 691.7031,-173 697.7031,-173 703.7031,-179 703.7031,-185 703.7031,-185 703.7031,-197 703.7031,-197 703.7031,-203 697.7031,-209 691.7031,-209 691.7031,-209 608.9287,-209 608.9287,-209 602.9287,-209 596.9287,-203 596.9287,-197 596.9287,-197 596.9287,-185 596.9287,-185 596.9287,-179 602.9287,-173 608.9287,-173"/>
<text text-anchor="middle" x="650.3159" y="-186.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">ACL manager</text>
<polygon fill="none" stroke="#ff8c00" points="514.7056,-173 514.7056,-209 609.8862,-209 609.8862,-173 514.7056,-173"/>
<text text-anchor="middle" x="562.2959" y="-186.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">ACL manager</text>
</g>
<!-- known_user_key&#45;&gt;acl_manager -->
<g id="edge9" class="edge">
<title>known_user_key&#45;&gt;acl_manager</title>
<path fill="none" stroke="#ff8c00" d="M325.3184,-180.8882C399.9907,-183.2115 525.6007,-187.1197 596.8147,-189.3354"/>
<text text-anchor="middle" x="463.3062" y="-190.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user matches an existing host</text>
<path fill="none" stroke="#ff8c00" d="M267.461,-177.4127C331.1153,-180.3462 438.21,-185.2816 504.3082,-188.3277"/>
<polygon fill="#ff8c00" stroke="#ff8c00" points="504.401,-191.8356 514.5516,-188.7997 504.7233,-184.843 504.401,-191.8356"/>
<text text-anchor="middle" x="393.4697" y="-188.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">user matches an existing host</text>
</g>
<!-- builtin_shell -->
<g id="node6" class="node">
<title>builtin_shell</title>
<path fill="none" stroke="#006400" d="M874.6738,-223C874.6738,-223 944.46,-223 944.46,-223 950.46,-223 956.46,-229 956.46,-235 956.46,-235 956.46,-247 956.46,-247 956.46,-253 950.46,-259 944.46,-259 944.46,-259 874.6738,-259 874.6738,-259 868.6738,-259 862.6738,-253 862.6738,-247 862.6738,-247 862.6738,-235 862.6738,-235 862.6738,-229 868.6738,-223 874.6738,-223"/>
<text text-anchor="middle" x="909.5669" y="-243.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">built&#45;in</text>
<text text-anchor="middle" x="909.5669" y="-229.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">config shell</text>
<polygon fill="none" stroke="#006400" points="761.6929,-223 761.6929,-259 848.855,-259 848.855,-223 761.6929,-223"/>
<text text-anchor="middle" x="805.2739" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#006400">built&#45;in shell</text>
</g>
<!-- known_user_key&#45;&gt;builtin_shell -->
<g id="edge6" class="edge">
<title>known_user_key&#45;&gt;builtin_shell</title>
<path fill="none" stroke="#006400" d="M325.3695,-196.5059C340.0986,-200.1819 355.8759,-203.652 370.7031,-206 550.8024,-234.5204 768.2909,-239.9067 862.3934,-240.8487"/>
<text text-anchor="middle" x="650.3159" y="-238.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">user=admin</text>
<path fill="none" stroke="#006400" d="M267.592,-193.0548C281.6792,-197.2785 297.0081,-201.3215 311.4316,-204 469.5409,-233.361 660.2348,-239.5693 751.4965,-240.7835"/>
<polygon fill="#006400" stroke="#006400" points="751.5568,-244.2844 761.5974,-240.9027 751.6394,-237.2848 751.5568,-244.2844"/>
<text text-anchor="middle" x="562.2959" y="-238.8" font-family="Times,serif" font-size="14.00" fill="#006400">user=admin</text>
</g>
<!-- err_and_exit -->
<g id="node9" class="node">
<title>err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M887.1152,-81C887.1152,-81 932.0186,-81 932.0186,-81 938.0186,-81 944.0186,-87 944.0186,-93 944.0186,-93 944.0186,-137 944.0186,-137 944.0186,-143 938.0186,-149 932.0186,-149 932.0186,-149 887.1152,-149 887.1152,-149 881.1152,-149 875.1152,-143 875.1152,-137 875.1152,-137 875.1152,-93 875.1152,-93 875.1152,-87 881.1152,-81 887.1152,-81"/>
<text text-anchor="middle" x="909.5669" y="-117.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">error</text>
<text text-anchor="middle" x="909.5669" y="-103.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff0000">and exit</text>
<polygon fill="none" stroke="#ff0000" points="759.5703,-106 759.5703,-142 850.9775,-142 850.9775,-106 759.5703,-106"/>
<text text-anchor="middle" x="805.2739" y="-119.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">error and exit</text>
</g>
<!-- known_user_key&#45;&gt;err_and_exit -->
<g id="edge11" class="edge">
<title>known_user_key&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M325.3049,-172.979C457.9451,-159.8165 770.2119,-128.8288 874.7656,-118.4535"/>
<text text-anchor="middle" x="650.3159" y="-148.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid user</text>
<path fill="none" stroke="#ff0000" d="M267.4808,-170.4741C378.1362,-160.9117 634.8943,-138.7236 748.9418,-128.868"/>
<polygon fill="#ff0000" stroke="#ff0000" points="749.5354,-132.3298 759.1969,-127.9818 748.9327,-125.3558 749.5354,-132.3298"/>
<text text-anchor="middle" x="562.2959" y="-151.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid user</text>
</g>
<!-- invite_manager -->
<g id="node4" class="node">
<title>invite_manager</title>
<path fill="none" stroke="#ff8c00" d="M604.9092,-17C604.9092,-17 695.7227,-17 695.7227,-17 701.7227,-17 707.7227,-23 707.7227,-29 707.7227,-29 707.7227,-41 707.7227,-41 707.7227,-47 701.7227,-53 695.7227,-53 695.7227,-53 604.9092,-53 604.9092,-53 598.9092,-53 592.9092,-47 592.9092,-41 592.9092,-41 592.9092,-29 592.9092,-29 592.9092,-23 598.9092,-17 604.9092,-17"/>
<text text-anchor="middle" x="650.3159" y="-30.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#ff8c00">invite manager</text>
<polygon fill="none" stroke="#ff8c00" points="512.5078,-17 512.5078,-53 612.084,-53 612.084,-17 512.5078,-17"/>
<text text-anchor="middle" x="562.2959" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#ff8c00">invite manager</text>
</g>
<!-- unknown_user_key&#45;&gt;invite_manager -->
<g id="edge10" class="edge">
<title>unknown_user_key&#45;&gt;invite_manager</title>
<path fill="none" stroke="#ff8c00" d="M334.0291,-77.6434C407.9842,-67.6724 523.7263,-52.0674 592.789,-42.7561"/>
<text text-anchor="middle" x="463.3062" y="-74.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff8c00">user=invite:&lt;token&gt;</text>
<path fill="none" stroke="#ff8c00" d="M274.7912,-80.5452C338.467,-70.4579 438.7527,-54.5711 502.4793,-44.4759"/>
<polygon fill="#ff8c00" stroke="#ff8c00" points="503.0528,-47.9288 512.382,-42.9071 501.9575,-41.015 503.0528,-47.9288"/>
<text text-anchor="middle" x="455.4386" y="-31.7071" font-family="Times,serif" font-size="14.00" fill="#ff8c00">user=invite:&lt;token&gt;</text>
</g>
<!-- unknown_user_key&#45;&gt;err_and_exit -->
<g id="edge13" class="edge">
<title>unknown_user_key&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M333.7181,-89.2908C439.591,-92.8626 637.1209,-99.7853 707.7227,-104 724.1917,-104.9832 728.2588,-105.9333 744.7227,-107 789.6129,-109.9084 841.4427,-112.2584 874.8164,-113.641"/>
<text text-anchor="middle" x="650.3159" y="-106.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">any other user</text>
<path fill="none" stroke="#ff0000" d="M274.4978,-89.2935C352.2933,-89.0083 492.8294,-90.6942 612.084,-104 628.7169,-105.8558 632.5001,-108.7473 649.084,-111 682.1267,-115.4884 719.327,-118.6586 749.132,-120.7442"/>
<polygon fill="#ff0000" stroke="#ff0000" points="749.133,-124.2522 759.347,-121.437 749.6068,-117.2683 749.133,-124.2522"/>
<text text-anchor="middle" x="562.2959" y="-106.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">any other user</text>
</g>
<!-- learn_key -->
<g id="node8" class="node">
<title>learn_key</title>
<path fill="none" stroke="#006400" d="M884.3911,-17C884.3911,-17 934.7427,-17 934.7427,-17 940.7427,-17 946.7427,-23 946.7427,-29 946.7427,-29 946.7427,-41 946.7427,-41 946.7427,-47 940.7427,-53 934.7427,-53 934.7427,-53 884.3911,-53 884.3911,-53 878.3911,-53 872.3911,-47 872.3911,-41 872.3911,-41 872.3911,-29 872.3911,-29 872.3911,-23 878.3911,-17 884.3911,-17"/>
<text text-anchor="middle" x="909.5669" y="-37.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">learn the</text>
<text text-anchor="middle" x="909.5669" y="-23.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">pub key</text>
<polygon fill="none" stroke="#006400" points="771.4272,-17 771.4272,-53 839.1206,-53 839.1206,-17 771.4272,-17"/>
<text text-anchor="middle" x="805.2739" y="-30.8" font-family="Times,serif" font-size="14.00" fill="#006400">learn key</text>
</g>
<!-- invite_manager&#45;&gt;learn_key -->
<g id="edge8" class="edge">
<title>invite_manager&#45;&gt;learn_key</title>
<path fill="none" stroke="#006400" d="M707.8521,-35C757.9748,-35 829.1828,-35 872.2155,-35"/>
<text text-anchor="middle" x="785.1982" y="-37.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">valid token</text>
<path fill="none" stroke="#006400" d="M612.3465,-35C656.1463,-35 719.1598,-35 761.1155,-35"/>
<polygon fill="#006400" stroke="#006400" points="761.3041,-38.5001 771.3041,-35 761.304,-31.5001 761.3041,-38.5001"/>
<text text-anchor="middle" x="685.8271" y="-37.8" font-family="Times,serif" font-size="14.00" fill="#006400">valid token</text>
</g>
<!-- invite_manager&#45;&gt;err_and_exit -->
<g id="edge14" class="edge">
<title>invite_manager&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M707.8521,-52.7546C759.019,-68.5437 832.1589,-91.1133 874.868,-104.2926"/>
<text text-anchor="middle" x="785.1982" y="-91.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">invalid token</text>
<path fill="none" stroke="#ff0000" d="M611.4661,-53.0105C651.6045,-67.7127 708.3017,-88.4802 750.0066,-103.7562"/>
<polygon fill="#ff0000" stroke="#ff0000" points="748.8708,-107.0676 759.4646,-107.2206 751.2785,-100.4946 748.8708,-107.0676"/>
<text text-anchor="middle" x="685.8271" y="-95.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">invalid token</text>
</g>
<!-- ssh_proxy -->
<g id="node7" class="node">
<title>ssh_proxy</title>
<path fill="none" stroke="#006400" d="M877.0117,-168C877.0117,-168 942.1221,-168 942.1221,-168 948.1221,-168 954.1221,-174 954.1221,-180 954.1221,-180 954.1221,-192 954.1221,-192 954.1221,-198 948.1221,-204 942.1221,-204 942.1221,-204 877.0117,-204 877.0117,-204 871.0117,-204 865.0117,-198 865.0117,-192 865.0117,-192 865.0117,-180 865.0117,-180 865.0117,-174 871.0117,-168 877.0117,-168"/>
<text text-anchor="middle" x="909.5669" y="-188.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">SSH proxy</text>
<text text-anchor="middle" x="909.5669" y="-174.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#006400">Jump&#45;Host</text>
<polygon fill="none" stroke="#006400" points="766.3516,-168 766.3516,-204 844.1963,-204 844.1963,-168 766.3516,-168"/>
<text text-anchor="middle" x="805.2739" y="-181.8" font-family="Times,serif" font-size="14.00" fill="#006400">SSH proxy</text>
</g>
<!-- acl_manager&#45;&gt;ssh_proxy -->
<g id="edge7" class="edge">
<title>acl_manager&#45;&gt;ssh_proxy</title>
<path fill="none" stroke="#006400" d="M704.0566,-192.4569C738.7694,-193.1138 784.9041,-193.4561 825.6738,-192 838.3694,-191.5466 852.1251,-190.7084 864.7541,-189.7993"/>
<text text-anchor="middle" x="785.1982" y="-195.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#006400">authorized</text>
<path fill="none" stroke="#006400" d="M610.0008,-192.3563C641.8818,-193.0022 684.7518,-193.37 722.5703,-192 733.3636,-191.609 744.9337,-190.9319 755.8983,-190.1699"/>
<polygon fill="#006400" stroke="#006400" points="756.4612,-193.6382 766.18,-189.4199 755.9519,-186.6568 756.4612,-193.6382"/>
<text text-anchor="middle" x="685.8271" y="-194.8" font-family="Times,serif" font-size="14.00" fill="#006400">authorized</text>
</g>
<!-- acl_manager&#45;&gt;err_and_exit -->
<g id="edge12" class="edge">
<title>acl_manager&#45;&gt;err_and_exit</title>
<path fill="none" stroke="#ff0000" d="M703.7163,-179.7682C743.1076,-170.9461 797.7781,-157.5732 844.6738,-142 854.6331,-138.6927 865.2245,-134.5604 874.8992,-130.5307"/>
<text text-anchor="middle" x="785.1982" y="-172.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#ff0000">unauthorized</text>
<path fill="none" stroke="#ff0000" d="M610.264,-178.009C646.3866,-168.197 697.1155,-154.3556 741.5703,-142 744.1794,-141.2748 746.8478,-140.5307 749.5426,-139.7772"/>
<polygon fill="#ff0000" stroke="#ff0000" points="750.6733,-143.0952 759.3567,-137.025 748.7831,-136.3552 750.6733,-143.0952"/>
<text text-anchor="middle" x="685.8271" y="-169.8" font-family="Times,serif" font-size="14.00" fill="#ff0000">unauthorized</text>
</g>
<!-- host_1 -->
<g id="node10" class="node">
<title>host_1</title>
<path fill="none" stroke="#0000ff" d="M1024.5425,-223C1024.5425,-223 1094.7456,-223 1094.7456,-223 1100.7456,-223 1106.7456,-229 1106.7456,-235 1106.7456,-235 1106.7456,-247 1106.7456,-247 1106.7456,-253 1100.7456,-259 1094.7456,-259 1094.7456,-259 1024.5425,-259 1024.5425,-259 1018.5425,-259 1012.5425,-253 1012.5425,-247 1012.5425,-247 1012.5425,-235 1012.5425,-235 1012.5425,-229 1018.5425,-223 1024.5425,-223"/>
<text text-anchor="middle" x="1059.644" y="-236.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host1</text>
<polygon fill="none" stroke="#0000ff" points="904.3086,-223 904.3086,-259 986.0908,-259 986.0908,-223 904.3086,-223"/>
<text text-anchor="middle" x="945.1997" y="-236.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">root@host1</text>
</g>
<!-- ssh_proxy&#45;&gt;host_1 -->
<g id="edge3" class="edge">
<title>ssh_proxy&#45;&gt;host_1</title>
<path fill="none" stroke="#0000ff" d="M954.5012,-202.6151C964.678,-206.3678 975.4382,-210.3275 985.46,-214 994.2108,-217.2067 1003.5469,-220.6149 1012.54,-223.8913"/>
<path fill="none" stroke="#0000ff" d="M844.2511,-201.3206C859.7986,-207.4318 877.9046,-214.5486 894.4551,-221.054"/>
<polygon fill="#0000ff" stroke="#0000ff" points="893.4017,-224.4006 903.9889,-224.8015 895.9624,-217.8858 893.4017,-224.4006"/>
</g>
<!-- host_2 -->
<g id="node11" class="node">
<title>host_2</title>
<path fill="none" stroke="#0000ff" d="M1005.46,-168C1005.46,-168 1113.8281,-168 1113.8281,-168 1119.8281,-168 1125.8281,-174 1125.8281,-180 1125.8281,-180 1125.8281,-192 1125.8281,-192 1125.8281,-198 1119.8281,-204 1113.8281,-204 1113.8281,-204 1005.46,-204 1005.46,-204 999.46,-204 993.46,-198 993.46,-192 993.46,-192 993.46,-180 993.46,-180 993.46,-174 999.46,-168 1005.46,-168"/>
<text text-anchor="middle" x="1059.644" y="-181.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">user@host2:2222</text>
<polygon fill="none" stroke="#0000ff" points="887.9775,-168 887.9775,-204 1002.4219,-204 1002.4219,-168 887.9775,-168"/>
<text text-anchor="middle" x="945.1997" y="-181.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">user@host2:2222</text>
</g>
<!-- ssh_proxy&#45;&gt;host_2 -->
<g id="edge4" class="edge">
<title>ssh_proxy&#45;&gt;host_2</title>
<path fill="none" stroke="#0000ff" d="M954.1887,-186C966.458,-186 980.0332,-186 993.2463,-186"/>
<path fill="none" stroke="#0000ff" d="M844.2511,-186C854.6959,-186 866.2954,-186 877.8023,-186"/>
<polygon fill="#0000ff" stroke="#0000ff" points="877.8592,-189.5001 887.8591,-186 877.8591,-182.5001 877.8592,-189.5001"/>
</g>
<!-- host_3 -->
<g id="node12" class="node">
<title>host_3</title>
<path fill="none" stroke="#0000ff" d="M1006.6392,-113C1006.6392,-113 1112.6489,-113 1112.6489,-113 1118.6489,-113 1124.6489,-119 1124.6489,-125 1124.6489,-125 1124.6489,-137 1124.6489,-137 1124.6489,-143 1118.6489,-149 1112.6489,-149 1112.6489,-149 1006.6392,-149 1006.6392,-149 1000.6392,-149 994.6392,-143 994.6392,-137 994.6392,-137 994.6392,-125 994.6392,-125 994.6392,-119 1000.6392,-113 1006.6392,-113"/>
<text text-anchor="middle" x="1059.644" y="-126.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#0000ff">root@host3:1234</text>
<polygon fill="none" stroke="#0000ff" points="888.3638,-113 888.3638,-149 1002.0356,-149 1002.0356,-113 888.3638,-113"/>
<text text-anchor="middle" x="945.1997" y="-126.8" font-family="Times,serif" font-size="14.00" fill="#0000ff">root@host3:1234</text>
</g>
<!-- ssh_proxy&#45;&gt;host_3 -->
<g id="edge5" class="edge">
<title>ssh_proxy&#45;&gt;host_3</title>
<path fill="none" stroke="#0000ff" d="M954.1887,-169.6471C971.9014,-163.1558 992.3359,-155.667 1010.4731,-149.0201"/>
<path fill="none" stroke="#0000ff" d="M844.2511,-170.6794C858.381,-165.1255 874.624,-158.7409 889.8921,-152.7395"/>
<polygon fill="#0000ff" stroke="#0000ff" points="891.2185,-155.9789 899.245,-149.0632 888.6578,-149.4641 891.2185,-155.9789"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,7 +1,7 @@
defaults: &defaults
working_directory: /go/src/moul.io/sshportal
docker:
- image: circleci/golang:1.14.4
- image: circleci/golang:1.11
environment:
GO111MODULE: "on"
@@ -16,6 +16,18 @@ install_retry: &install_retry
version: 2
jobs:
go.build:
<<: *defaults
steps:
- checkout
- *install_retry
- run: /tmp/retry -m 3 go mod download
- run: /tmp/retry -m 3 go mod vendor
- run: /tmp/retry -m 3 make install
- run: GO111MODULE=off /tmp/retry -m 3 go test -v ./...
- run: /tmp/retry -m 3 curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.12.2
- run: PATH=$PATH:$(pwd)/bin /tmp/retry -m 3 make lint
docker.integration:
<<: *defaults
steps:
@@ -24,7 +36,7 @@ jobs:
name: Install Docker Compose
command: |
umask 022
curl -L https://github.com/docker/compose/releases/download/1.11.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
- setup_remote_docker:
docker_layer_caching: true
- *install_retry
@@ -36,4 +48,6 @@ workflows:
version: 2
build_and_integration:
jobs:
- go.build
- docker.integration
# requires: docker.build?

View File

@@ -1,5 +1,3 @@
# .git/ # should be kept for git-based versionning
examples/
.circleci/
.assets/

6
.github/FUNDING.yml vendored
View File

@@ -1,6 +0,0 @@
github: ["moul"]
patreon: moul
open_collective: sshportal
custom:
- "https://www.buymeacoffee.com/moul"
- "https://manfred.life/donate"

View File

@@ -1,88 +0,0 @@
name: CI
on:
push:
tags:
- v*
branches:
- master
pull_request:
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: docker build . --file Dockerfile
golangci-lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: lint
uses: golangci/golangci-lint-action@v0.1.7
with:
version: v1.26
github-token: ${{ secrets.GITHUB_TOKEN }}
tests-on-windows:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: windows-latest
strategy:
matrix:
golang:
#- 1.13
- 1.14
steps:
- uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- name: Run tests on Windows
run: make.exe unittest
continue-on-error: true
tests-on-mac:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: macos-latest
strategy:
matrix:
golang:
- 1.14
steps:
- uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.golang }}-
- name: Run tests on Unix-like operating systems
run: make unittest
tests-on-linux:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: ubuntu-latest
strategy:
matrix:
golang:
- 1.11
- 1.12
- 1.13
- 1.14
steps:
- uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.golang }}-
- name: Run tests on Unix-like operating systems
run: make unittest

View File

@@ -1,13 +0,0 @@
name: Semantic Release
on: push
jobs:
semantic-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: codfish/semantic-release-action@v1
if: github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

6
.gitignore vendored
View File

@@ -1,9 +1,3 @@
coverage.txt
dist/
*~
*#
.*#
.DS_Store
/log/
/sshportal
*.db

View File

@@ -1,10 +1,8 @@
run:
deadline: 1m
tests: false
skip-files:
- "testing.go"
- ".*\\.pb\\.go"
- ".*\\.gen\\.go"
#skip-files:
# - ".*\\.gen\\.go"
linters-settings:
golint:
@@ -20,36 +18,17 @@ linters-settings:
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
#- dupl
- errcheck
#- funlen
- gochecknoinits
#- gocognit
- goconst
- gocritic
#- gocyclo
- misspell
- deadcode
- misspell
- structcheck
- errcheck
- unused
- varcheck
- staticcheck
- unconvert
- gofmt
- goimports
- golint
- gosimple
- govet
- ineffassign
- interfacer
#- maligned
- misspell
- nakedret
- prealloc
- scopelint
- staticcheck
- structcheck
#- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace

View File

@@ -1,29 +0,0 @@
builds:
-
goos: [linux, darwin]
goarch: [386, amd64, arm, arm64]
ldflags:
- -s -w -X main.GitSha={{.ShortCommit}} -X main.GitBranch=master -X main.GitTag={{.Version}}
archives:
- wrap_in_directory: true
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
brews:
-
name: sshportal
github:
owner: moul
name: homebrew-moul
commit_author:
name: moul-bot
email: "m+bot@42.am"
homepage: https://manfred.life/sshportal
description: "Simple, fun and transparent SSH (and telnet) bastion"

View File

@@ -1,8 +0,0 @@
module.exports = {
branch: 'master',
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/github',
],
};

29
AUTHORS
View File

@@ -1,29 +0,0 @@
# This file lists all individuals having contributed content to the repository.
# For how it is generated, see 'https://github.com/moul/rules.mk'
ahh <ahamidullah@gmail.com>
Alen Masic <alenn.masic@gmail.com>
Alexander Turner <me@alexturner.co>
bozzo <bozzo@users.noreply.github.com>
fossabot <badges@fossa.io>
Jean-Louis Férey <jeanlouis.ferey@orange.com>
Jess <jessachandler@gmail.com>
Jonathan Lestrelin <jonathan.lestrelin@gmail.com>
Julien Dessaux <julien.dessaux@adyxax.org>
Manfred Touron <94029+moul@users.noreply.github.com>
Manfred Touron <m@42.am>
Manuel <manuel.sabban@nbs-system.com>
Manuel Sabban <manu@sabban.eu>
Manuel Sabban <msa@nbs-system.com>
Mathieu Pasquet <mathieu.pasquet@alterway.fr>
Mikael Rapp <micke.rapp@gmail.com>
MitaliBo <mitali.bisht14@gmail.com>
Nelly Asher <karmelylle@rambler.ru>
NocFlame <aad@nocflame.se>
Quentin Perez <qperez42@gmail.com>
Renovate Bot <bot@renovateapp.com>
Shawn Wang <shawn111@gmail.com>
Valentin Daviot <valentin.daviot@alterway.fr>
valentin.daviot <valentin.daviot@alterway.fr>
welderpb <welderpb@users.noreply.github.com>
Дмитрий Шульгачик <tech@uniplug.ru>

View File

@@ -2,21 +2,7 @@
## master (unreleased)
* No entry
## v1.10.0 (2019-06-24)
* Bump deps, now using github.com/gliderlabs/ssh upstream
* Fix Windows build ([#101](https://github.com/moul/sshportal/pull/101)) by [@Raerten](https://github.com/Raerten)
* Use environment variables for settings ([#98](https://github.com/moul/sshportal/pull/98)) by [@Raerten](https://github.com/Raerten)
* Fix 'userkey create' ([#111](https://github.com/moul/sshportal/pull/111)) by [@shawn111](https://github.com/shawn111)
* Set log files mode to 440 instead of 640 ([#134](https://github.com/moul/sshportal/pull/134)) by [@jle64](https://github.com/jle64)
* Allow to create a host using an IP as name ([#135](https://github.com/moul/sshportal/pull/135)) by [@jle64](https://github.com/jle64)
* Add username and session ID to session log filename ([#133](https://github.com/moul/sshportal/pull/133)) by [@jle64](https://github.com/jle64)
* Unable to use encrypted SSH private keys ([#124](https://github.com/moul/sshportal/pull/124)) by [@welderpb](https://github.com/welderpb)
* Fix format of ID in new session + closing channel if host is unreachable ([#123](https://github.com/moul/sshportal/pull/123)) by [@vdaviot](https://github.com/vdaviot)
* Refactor the main package with a focus on splitting up into packages ([#113](https://github.com/moul/sshportal/pull/113)) by [@ahamidullah](https://github.com/ahamidullah)
* Bump deps
## v1.9.0 (2018-11-18)

View File

@@ -1,10 +1,7 @@
# build
FROM golang:1.14.4 as builder
ENV GO111MODULE=on
FROM golang:1.11 as builder
COPY . /go/src/moul.io/sshportal
WORKDIR /go/src/moul.io/sshportal
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN make _docker_install
# minimal runtime

View File

@@ -1,15 +1,18 @@
GOPKG ?= moul.io/sshportal
GOBINS ?= .
DOCKER_IMAGE ?= moul/sshportal
VERSION ?= `git describe --tags --always`
VCS_REF ?= `git rev-parse --short HEAD`
GO_INSTALL_OPTS = -ldflags="-X main.GitSha=$(VCS_REF) -X main.GitTag=$(VERSION)"
include rules.mk
DB_VERSION ?= v$(shell grep -E 'ID: "[0-9]+",' pkg/bastion/dbinit.go | tail -n 1 | cut -d'"' -f2)
GIT_SHA ?= $(shell git rev-parse HEAD)
GIT_TAG ?= $(shell git describe --tags --always)
GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS ?= -X main.GitSha=$(GIT_SHA) -X main.GitTag=$(GIT_TAG) -X main.GitBranch=$(GIT_BRANCH)
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
AES_KEY ?= my-dummy-aes-key
GO ?= GO111MODULE=on go
.PHONY: install
install:
$(GO) install -v -ldflags '$(LDFLAGS)' .
.PHONY: docker.build
docker.build:
docker build -t moul/sshportal .
.PHONY: integration
integration:
@@ -24,23 +27,21 @@ dev:
-$(GO) get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
.PHONY: test
test:
$(GO) test -i ./...
$(GO) test -v ./...
.PHONY: lint
lint:
golangci-lint run --verbose ./...
.PHONY: backup
backup:
mkdir -p data/backups
cp sshportal.db data/backups/$(shell date +%s)-$(DB_VERSION)-sshportal.sqlite
cp sshportal.db data/backups/$(shell date +%s)-$(VERSION)-sshportal.sqlite
doc:
dot -Tsvg ./.assets/overview.dot > ./.assets/overview.svg
dot -Tsvg ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.svg
dot -Tsvg ./.assets/flow-diagram.dot > ./.assets/flow-diagram.svg
dot -Tpng ./.assets/overview.dot > ./.assets/overview.png
dot -Tpng ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.png
dot -Tpng ./.assets/flow-diagram.dot > ./.assets/flow-diagram.png
.PHONY: goreleaser
goreleaser:
GORELEASER_GITHUB_TOKEN=$(GORELEASER_GITHUB_TOKEN) GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist
.PHONY: goreleaser-dry-run
goreleaser-dry-run:
goreleaser --snapshot --skip-publish --rm-dist

424
README.md
View File

@@ -1,43 +1,65 @@
# sshportal
[![CircleCI](https://circleci.com/gh/moul/sshportal.svg?style=svg)](https://circleci.com/gh/moul/sshportal)
[![Go Report Card](https://goreportcard.com/badge/moul.io/sshportal)](https://goreportcard.com/report/moul.io/sshportal)
[![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/)
[![Go Report Card](https://goreportcard.com/moul.io/sshportal)](https://goreportcard.com/report/moul.io/sshportal)
[![GoDoc](https://godoc.org/moul.io/sshportal?status.svg)](https://godoc.org/moul.io/sshportal)
[![Financial Contributors on Open Collective](https://opencollective.com/sshportal/all/badge.svg?label=financial+contributors)](https://opencollective.com/sshportal) [![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/moul/sshportal.svg)](https://github.com/moul/sshportal/releases)
<!-- temporarily broken? [![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/) -->
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_shield)
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
Features include: independence of users and hosts, convenient user invite system, connecting to servers that don't support SSH keys, various levels of access, and many more. Easy to install, run and configure.
![Flow Diagram](https://raw.githubusercontent.com/moul/sshportal/master/.assets/flow-diagram.png)
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
---
## Contents
## Overview
<!-- toc -->
![sshportal overview](https://raw.github.com/moul/sshportal/master/.assets/overview.svg?sanitize=true)
- [Installation and usage](#installation-and-usage)
- [Use cases](#use-cases)
- [Features and limitations](#features-and-limitations)
- [Docker](#docker)
- [Manual Install](#manual-install)
- [Backup / Restore](#backup--restore)
- [built-in shell](#built-in-shell)
- [Demo data](#demo-data)
- [Shell commands](#shell-commands)
- [Healthcheck](#healthcheck)
- [portal alias (.ssh/config)](#portal-alias-sshconfig)
- [Scaling](#scaling)
- [Under the hood](#under-the-hood)
## Features
<!-- tocstop -->
* Single autonomous binary (~10-20Mb) with no runtime dependencies (embeds ssh server and client)
* Portable / Cross-platform (regularly tested on linux and OSX/darwin)
* Store data in [Sqlite3](https://www.sqlite.org/) or [MySQL](https://www.mysql.com) (probably easy to add postgres, mssql thanks to gorm)
* Stateless -> horizontally scalable when using [MySQL](https://www.mysql.com) as the backend
* Connect to remote host using key or password
* Admin commands can be run directly or in an interactive shell
* Host management
* User management (invite, group, stats)
* Host Key management (create, remove, update, import)
* Automatic remote host key learning
* User Key management (multile keys per user)
* ACL management (acl+user-groups+host-groups)
* User roles (admin, trusted, standard, ...)
* User invitations (no more "give me your public ssh key please")
* Easy server installation (generate shell command to setup `authorized_keys`)
* Sensitive data encryption
* Session management (see active connections, history, stats, stop)
* Audit log (logging every user action)
* Record TTY Session
* Tunnels logging
* Host Keys verifications shared across users
* Healthcheck user (replying OK to any user)
* SSH compatibility
* ipv4 and ipv6 support
* [`scp`](https://linux.die.net/man/1/scp) support
* [`rsync`](https://linux.die.net/man/1/rsync) support
* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support
* [`sftp`](https://www.ssh.com/ssh/sftp/) support
* [`ssh-agent`](https://www.ssh.com/ssh/agent) support
* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support
* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server)
* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients
* SSH to non-SSH proxy
* [Telnet](https://www.ssh.com/ssh/telnet) support
---
## (Known) limitations
## Installation and usage
* Does not work (yet?) with [`mosh`](https://mosh.org/)
## Usage
Start the server
@@ -58,8 +80,6 @@ Shared connection to localhost closed.
$
```
If the association fails and you are promted for a password, verify that the host you're connecting from has a SSH key set up or generate one with ```ssh-keygen -t rsa```
Drop an interactive administrator shell
```console
@@ -119,159 +139,15 @@ To associate this account with a key, use the following SSH user: 'invite:NfHK5a
config>
```
Demo gif:
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
## Flow Diagram
---
## Use cases
Used by educators to provide temporary access to students. [Feedback from a teacher](https://github.com/moul/sshportal/issues/64). The author is using it in one of his projects, *pathwar*, to dynamically configure hosts and users, so that he can give temporary accesses for educational purposes.
*vptech*, the vente-privee.com technical team (a group of over 6000 people) is using it internally to manage access to servers/routers, saving hours on configuration management and not having to share the configuration information.
There are companies who use a jump host to monitor connections at a single point.
A hosting company is using SSHportal for its “logging” feature, among the others. As every session is logged and introspectable, they have a detailed history of who performed which action. This company made its own contribution on the project, allowing the support of [more than 65.000 sessions in the database](https://github.com/moul/sshportal/pull/76).
The project has also received [multiple contributions from a security researcher](https://github.com/moul/sshportal/pulls?q=is%3Apr+author%3Asabban+sort%3Aupdated-desc) that made a thesis on quantum cryptography. This person uses SSHportal in their security-hardened hosting company.
If you need to invite multiple people to an event (hackathon, course, etc), the day before the event you can create multiple accounts at once, print the invite, and distribute the paper.
---
## Features and limitations
* Single autonomous binary (~10-20Mb) with no runtime dependencies (embeds ssh server and client)
* Portable / Cross-platform (regularly tested on linux and OSX/darwin)
* Store data in [Sqlite3](https://www.sqlite.org/) or [MySQL](https://www.mysql.com) (probably easy to add postgres, mssql thanks to gorm)
* Stateless -> horizontally scalable when using [MySQL](https://www.mysql.com) as the backend
* Connect to remote host using key or password
* Admin commands can be run directly or in an interactive shell
* Host management
* User management (invite, group, stats)
* Host Key management (create, remove, update, import)
* Automatic remote host key learning
* User Key management (multile keys per user)
* ACL management (acl+user-groups+host-groups)
* User roles (admin, trusted, standard, ...)
* User invitations (no more "give me your public ssh key please")
* Easy server installation (generate shell command to setup `authorized_keys`)
* Sensitive data encryption
* Session management (see active connections, history, stats, stop)
* Audit log (logging every user action)
* Record TTY Session (with [ttyrec](https://en.wikipedia.org/wiki/Ttyrec) format, use `ttyplay` for replay)
* Tunnels logging
* Host Keys verifications shared across users
* Healthcheck user (replying OK to any user)
* SSH compatibility
* ipv4 and ipv6 support
* [`scp`](https://linux.die.net/man/1/scp) support
* [`rsync`](https://linux.die.net/man/1/rsync) support
* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support
* [`sftp`](https://www.ssh.com/ssh/sftp/) support
* [`ssh-agent`](https://www.ssh.com/ssh/agent) support
* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support
* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server)
* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients
* SSH to non-SSH proxy
* [Telnet](https://www.ssh.com/ssh/telnet) support
**(Known) limitations**
* Does not work (yet?) with [`mosh`](https://mosh.org/)
* It is not possible for a user to access a host with the same name as the user. This is easily circumvented by changing the user name, especially since the most common use cases does not expose it.
* It is not possible access a host named `healthcheck` as this is a built in command.
---
## Docker
Docker is the recommended way to run sshportal.
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
```
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using an old version and you want to use the new version v1.10.0
# stop and rename the last working container + backup the database
docker stop sshportal
docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
Now you can test ssh-ing to sshportal to check if everything looks OK.
In case of problem, you can rollback to the latest working version with the latest working backup, using:
```sh
docker stop sshportal
docker rm sshportal
cp sshportal.db.bkp sshportal.db
docker rename sshportal_old sshportal
docker start sshportal
docker logs -f sshportal
```
---
## Manual Install
Get the latest version using GO.
```sh
GO111MODULE=on go get -u moul.io/sshportal
```
---
## Backup / Restore
sshportal embeds built-in backup/restore methods which basically import/export JSON objects:
```sh
# Backup
ssh portal config backup > sshportal.bkp
# Restore
ssh portal config restore < sshportal.bkp
```
This method is particularly useful as it should be resistant against future DB schema changes (expected during development phase).
I suggest you to be careful during this development phase, and use an additional backup method, for example:
```sh
# sqlite dump
sqlite3 sshportal.db .dump > sshportal.sql.bkp
# or just the immortal cp
cp sshportal.db sshportal.db.bkp
```
---
![Flow Diagram](https://raw.github.com/moul/sshportal/master/.assets/flow-diagram.svg?sanitize=true)
## built-in shell
`sshportal` embeds a configuration CLI.
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server. The shell is also accessible through `ssh [username]@portal.example.org`.
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
@@ -281,29 +157,7 @@ ssh admin@portal.example.org host inspect toto
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
![sshportal overview](https://raw.github.com/moul/sshportal/master/.assets/overview.png)
---
## Demo data
The following servers are freely available, without external registration,
it makes it easier to quickly test `sshportal` without configuring your own servers to accept sshportal connections.
```
ssh portal host create new@sdf.org
ssh sdf@portal
ssh portal host create test@whoami.filippo.io
ssh whoami@portal
ssh portal host create test@chat.shazow.net
ssh chat@portal
```
---
## Shell commands
### Synopsis
```sh
# acl management
@@ -376,7 +230,120 @@ info [-h]
version [-h]
```
---
## Docker
Docker is the recommended way to run sshportal.
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.9.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
```
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using an old version and you want to use the new version v1.9.0
# stop and rename the last working container + backup the database
docker stop sshportal
docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.9.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
Now you can test ssh-ing to sshportal to check if everything looks OK.
In case of problem, you can rollback to the latest working version with the latest working backup, using:
```sh
docker stop sshportal
docker rm sshportal
cp sshportal.db.bkp sshportal.db
docker rename sshportal_old sshportal
docker start sshportal
docker logs -f sshportal
```
## Manual Install
Get the latest version using GO.
```sh
go get -u moul.io/sshportal
```
## portal alias (.ssh/config)
Edit your `~/.ssh/config` file (create it first if needed)
```ini
Host portal
User admin
Port 2222 # portal port
HostName 127.0.0.1 # portal hostname
```
```bash
# you can now run a shell using this:
ssh portal
# instead of this:
ssh localhost -p 2222 -l admin
# or connect to hosts using this:
ssh hostname@portal
# instead of this:
ssh localhost -p 2222 -l hostname
```
## Backup / Restore
sshportal embeds built-in backup/restore methods which basically import/export JSON objects:
```sh
# Backup
ssh portal config backup > sshportal.bkp
# Restore
ssh portal config restore < sshportal.bkp
```
This method is particularly useful as it should be resistant against future DB schema changes (expected during development phase).
I suggest you to be careful during this development phase, and use an additional backup method, for example:
```sh
# sqlite dump
sqlite3 sshportal.db .dump > sshportal.sql.bkp
# or just the immortal cp
cp sshportal.db sshportal.db.bkp
```
## Demo data
The following servers are freely available, without external registration,
it makes it easier to quickly test `sshportal` without configuring your own servers to accept sshportal connections.
```
ssh portal host create new@sdf.org
ssh sdf@portal
ssh portal host create test@whoami.filippo.io
ssh whoami@portal
ssh portal host create test@chat.shazow.net
ssh chat@portal
```
## Healthcheck
@@ -410,33 +377,6 @@ $ sshportal healthcheck --wait && ssh sshportal -l admin
config>
```
---
## portal alias (.ssh/config)
Edit your `~/.ssh/config` file (create it first if needed)
```ini
Host portal
User admin
Port 2222 # portal port
HostName 127.0.0.1 # portal hostname
```
```bash
# you can now run a shell using this:
ssh portal
# instead of this:
ssh localhost -p 2222 -l admin
# or connect to hosts using this:
ssh hostname@portal
# instead of this:
ssh localhost -p 2222 -l hostname
```
---
## Scaling
`sshportal` is stateless but relies on a database to store configuration and logs.
@@ -445,12 +385,10 @@ By default, `sshportal` uses a local [sqlite](https://www.sqlite.org/) database
You can run multiple instances of `sshportal` sharing a same [MySQL](https://www.mysql.com) database, using `sshportal --db-conn=user:pass@host/dbname?parseTime=true --db-driver=mysql`.
![sshportal cluster with MySQL backend](https://raw.github.com/moul/sshportal/master/.assets/cluster-mysql.png)
![sshportal cluster with MySQL backend](https://raw.github.com/moul/sshportal/master/.assets/cluster-mysql.svg?sanitize=true)
See [examples/mysql](http://github.com/moul/sshportal/tree/master/examples/mysql).
---
## Under the hood
* Docker first (used in dev, tests, by the CI and in production)
@@ -468,34 +406,12 @@ See [examples/mysql](http://github.com/moul/sshportal/tree/master/examples/mysql
* https://github.com/mgutz/ansi: Terminal color helpers
* https://github.com/urfave/cli: CLI flag parsing with subcommands support
![sshportal data model](https://raw.github.com/moul/sshportal/master/.assets/sql-schema.png)
![sshportal data model](https://raw.github.com/moul/sshportal/master/.assets/sql-schema.svg?sanitize=true)
## Contributors
## Note
### Code Contributors
This is totally experimental for now, so please file issues to let me know what you think about it!
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/moul/sshportal/graphs/contributors"><img src="https://opencollective.com/sshportal/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/sshportal/contribute)]
#### Individuals
<a href="https://opencollective.com/sshportal"><img src="https://opencollective.com/sshportal/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/sshportal/contribute)]
<a href="https://opencollective.com/sshportal/organization/0/website"><img src="https://opencollective.com/sshportal/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/1/website"><img src="https://opencollective.com/sshportal/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/2/website"><img src="https://opencollective.com/sshportal/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/3/website"><img src="https://opencollective.com/sshportal/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/4/website"><img src="https://opencollective.com/sshportal/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/5/website"><img src="https://opencollective.com/sshportal/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/6/website"><img src="https://opencollective.com/sshportal/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/7/website"><img src="https://opencollective.com/sshportal/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/8/website"><img src="https://opencollective.com/sshportal/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/9/website"><img src="https://opencollective.com/sshportal/organization/9/avatar.svg"></a>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large)

View File

@@ -1 +0,0 @@
theme: jekyll-theme-slate

40
acl.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import "sort"
type ByWeight []*ACL
func (a ByWeight) Len() int { return len(a) }
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func CheckACLs(user User, host Host) (string, error) {
// shared ACLs between user and host
aclMap := map[uint]*ACL{}
for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups {
for _, hostGroupACL := range hostGroup.ACLs {
if userGroupACL.ID == hostGroupACL.ID {
aclMap[userGroupACL.ID] = userGroupACL
}
}
}
}
}
// FIXME: add ACLs that match host pattern
// deny by default if no shared ACL
if len(aclMap) == 0 {
return string(ACLActionDeny), nil // default action
}
// transform map to slice and sort it
acls := make([]*ACL, 0, len(aclMap))
for _, acl := range aclMap {
acls = append(acls, acl)
}
sort.Sort(ByWeight(acls))
return acls[0].Action, nil
}

47
acl_test.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/jinzhu/gorm"
. "github.com/smartystreets/goconvey/convey"
)
func TestCheckACLs(t *testing.T) {
Convey("Testing CheckACLs", t, func() {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
So(err, ShouldBeNil)
defer func() {
So(os.RemoveAll(tempDir), ShouldBeNil)
}()
// create sqlite db
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
So(err, ShouldBeNil)
db.LogMode(false)
So(dbInit(db), ShouldBeNil)
// create dummy objects
var hostGroup HostGroup
err = HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
So(err, ShouldBeNil)
db.Create(&Host{Groups: []*HostGroup{&hostGroup}})
//. load db
var (
hosts []Host
users []User
)
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action, err := CheckACLs(users[0], hosts[0])
So(err, ShouldBeNil)
So(action, ShouldEqual, ACLActionAllow)
})
}

52
config.go Normal file
View File

@@ -0,0 +1,52 @@
package main
import (
"fmt"
"os"
"time"
"github.com/urfave/cli"
)
type configServe struct {
aesKey string
dbDriver, dbURL string
logsLocation string
bindAddr string
debug, demo bool
idleTimeout time.Duration
}
func parseServeConfig(c *cli.Context) (*configServe, error) {
ret := &configServe{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
default:
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
return ret, nil
}
func ensureLogDirectory(location string) error {
// check for the logdir existence
logsLocation, err := os.Stat(location)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
}
return err
}
if !logsLocation.IsDir() {
return fmt.Errorf("log directory cannot be created")
}
return nil
}

View File

@@ -1,4 +1,4 @@
package crypto // import "moul.io/sshportal/pkg/crypto"
package main
import (
"bytes"
@@ -15,11 +15,10 @@ import (
"strings"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/dbmodels"
)
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
key := SSHKey{
Type: keyType,
Length: length,
}
@@ -54,8 +53,8 @@ func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
return &key, nil
}
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
func ImportSSHKey(keyValue string) (*SSHKey, error) {
key := SSHKey{
Type: "rsa",
}
@@ -133,7 +132,7 @@ func safeDecrypt(key []byte, cryptoText string) string {
return out
}
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
func HostEncrypt(aesKey string, host *Host) (err error) {
if aesKey == "" {
return nil
}
@@ -142,7 +141,7 @@ func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
}
return
}
func HostDecrypt(aesKey string, host *dbmodels.Host) {
func HostDecrypt(aesKey string, host *Host) {
if aesKey == "" {
return
}
@@ -151,14 +150,14 @@ func HostDecrypt(aesKey string, host *dbmodels.Host) {
}
}
func SSHKeyEncrypt(aesKey string, key *dbmodels.SSHKey) (err error) {
func SSHKeyEncrypt(aesKey string, key *SSHKey) (err error) {
if aesKey == "" {
return nil
}
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
return
}
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
if aesKey == "" {
return
}

View File

@@ -1,14 +1,16 @@
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
"github.com/jinzhu/gorm"
gossh "golang.org/x/crypto/ssh"
)
@@ -51,7 +53,7 @@ type SSHKey struct {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32)"`
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
@@ -68,7 +70,7 @@ type Host struct {
// UserKey defines a user public key used by sshportal to identify the user
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000" valid:"length(1|10000)"`
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
@@ -164,8 +166,30 @@ const (
BastionSchemeTelnet BastionScheme = "telnet"
)
func init() {
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
name, ok := i.(string)
if !ok {
return false
}
return unixUserRegexp.MatchString(name)
}))
}
// Host helpers
func ParseInputURL(input string) (*url.URL, error) {
if !strings.Contains(input, "://") {
input = "ssh://" + input
}
u, err := url.Parse(input)
if err != nil {
return nil, err
}
return u, nil
}
func (host *Host) DialAddr() string {
return fmt.Sprintf("%s:%d", host.Hostname(), host.Port())
}
@@ -277,7 +301,7 @@ func HostByName(db *gorm.DB, name string) (*Host, error) {
return &host, nil
}
func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.Username(),
HostKeyCallback: hk,

View File

@@ -1,23 +1,20 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
package main
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/user"
"strings"
"time"
"github.com/go-gormigrate/gormigrate"
"github.com/jinzhu/gorm"
gossh "golang.org/x/crypto/ssh"
gormigrate "gopkg.in/gormigrate.v1"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
)
func DBInit(db *gorm.DB) error {
func dbInit(db *gorm.DB) error {
log.SetOutput(ioutil.Discard)
db.Callback().Delete().Replace("gorm:delete", hardDeleteCallback)
log.SetOutput(os.Stderr)
@@ -46,9 +43,9 @@ func DBInit(db *gorm.DB) error {
Type string
Length uint
Fingerprint string
PrivKey string `sql:"size:10000"`
PubKey string `sql:"size:10000"`
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
PrivKey string `sql:"size:10000"`
PubKey string `sql:"size:10000"`
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
Comment string
}
return tx.AutoMigrate(&SSHKey{}).Error
@@ -66,9 +63,9 @@ func DBInit(db *gorm.DB) error {
Addr string
User string
Password string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string
Comment string
}
@@ -82,9 +79,9 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Key []byte `sql:"size:10000"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string
}
return tx.AutoMigrate(&UserKey{}).Error
@@ -101,8 +98,8 @@ func DBInit(db *gorm.DB) error {
IsAdmin bool
Email string
Name string
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string
InviteToken string
}
@@ -117,8 +114,8 @@ func DBInit(db *gorm.DB) error {
type UserGroup struct {
gorm.Model
Name string
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
Users []*User `gorm:"many2many:user_user_groups;"`
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
Comment string
}
return tx.AutoMigrate(&UserGroup{}).Error
@@ -132,8 +129,8 @@ func DBInit(db *gorm.DB) error {
type HostGroup struct {
gorm.Model
Name string
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
Hosts []*Host `gorm:"many2many:host_host_groups;"`
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
Comment string
}
return tx.AutoMigrate(&HostGroup{}).Error
@@ -146,8 +143,8 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type ACL struct {
gorm.Model
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string
Action string
Weight uint
@@ -162,64 +159,64 @@ func DBInit(db *gorm.DB) error {
}, {
ID: "9",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name")
return db.Model(&dbmodels.Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
db.Model(&Setting{}).RemoveIndex("uix_settings_name")
return db.Model(&Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name").Error
return db.Model(&Setting{}).RemoveIndex("uix_settings_name").Error
},
}, {
ID: "10",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name")
return db.Model(&dbmodels.SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
db.Model(&SSHKey{}).RemoveIndex("uix_keys_name")
return db.Model(&SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name").Error
return db.Model(&SSHKey{}).RemoveIndex("uix_keys_name").Error
},
}, {
ID: "11",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name")
return db.Model(&dbmodels.Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
db.Model(&Host{}).RemoveIndex("uix_hosts_name")
return db.Model(&Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name").Error
return db.Model(&Host{}).RemoveIndex("uix_hosts_name").Error
},
}, {
ID: "12",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name")
return db.Model(&dbmodels.User{}).AddUniqueIndex("uix_users_name", "name").Error
db.Model(&User{}).RemoveIndex("uix_users_name")
return db.Model(&User{}).AddUniqueIndex("uix_users_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name").Error
return db.Model(&User{}).RemoveIndex("uix_users_name").Error
},
}, {
ID: "13",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name")
return db.Model(&dbmodels.UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name")
return db.Model(&UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name").Error
return db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
},
}, {
ID: "14",
Migrate: func(tx *gorm.DB) error {
db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name")
return db.Model(&dbmodels.HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name")
return db.Model(&HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
},
Rollback: func(tx *gorm.DB) error {
return db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
return db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
},
}, {
ID: "15",
Migrate: func(tx *gorm.DB) error {
type UserRole struct {
gorm.Model
Name string `valid:"required,length(1|32),unix_user"`
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
Name string `valid:"required,length(1|32),unix_user"`
Users []*User `gorm:"many2many:user_user_roles"`
}
return tx.AutoMigrate(&UserRole{}).Error
},
@@ -232,13 +229,13 @@ func DBInit(db *gorm.DB) error {
type User struct {
gorm.Model
IsAdmin bool
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
Roles []*UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
}
return tx.AutoMigrate(&User{}).Error
},
@@ -248,27 +245,27 @@ func DBInit(db *gorm.DB) error {
}, {
ID: "17",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
return tx.Create(&UserRole{Name: "admin"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "admin").Delete(&dbmodels.UserRole{}).Error
return tx.Where("name = ?", "admin").Delete(&UserRole{}).Error
},
}, {
ID: "18",
Migrate: func(tx *gorm.DB) error {
var adminRole dbmodels.UserRole
var adminRole UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
var users []*dbmodels.User
var users []User
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
return err
}
for _, user := range users {
user.Roles = append(user.Roles, &adminRole)
if err := tx.Save(user).Error; err != nil {
if err := tx.Save(&user).Error; err != nil {
return err
}
}
@@ -282,13 +279,13 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type User struct {
gorm.Model
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
Roles []*UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
}
return tx.AutoMigrate(&User{}).Error
},
@@ -298,24 +295,24 @@ func DBInit(db *gorm.DB) error {
}, {
ID: "20",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
return tx.Create(&UserRole{Name: "listhosts"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "listhosts").Delete(&dbmodels.UserRole{}).Error
return tx.Where("name = ?", "listhosts").Delete(&UserRole{}).Error
},
}, {
ID: "21",
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt time.Time `valid:"optional"`
Status string `valid:"required"`
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
StoppedAt time.Time `valid:"optional"`
Status string `valid:"required"`
User *User `gorm:"ForeignKey:UserID"`
Host *Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Session{}).Error
},
@@ -327,12 +324,12 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type Event struct {
gorm.Model
Author *dbmodels.User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
Author *User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
}
return tx.AutoMigrate(&Event{}).Error
},
@@ -344,11 +341,11 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
Key []byte `sql:"size:10000" valid:"required,length(1|10000)"`
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&UserKey{}).Error
},
@@ -358,7 +355,7 @@ func DBInit(db *gorm.DB) error {
}, {
ID: "24",
Migrate: func(tx *gorm.DB) error {
var userKeys []*dbmodels.UserKey
var userKeys []UserKey
if err := db.Find(&userKeys).Error; err != nil {
return err
}
@@ -369,7 +366,7 @@ func DBInit(db *gorm.DB) error {
return err
}
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
if err := db.Model(userKey).Updates(userKey).Error; err != nil {
if err := db.Model(&userKey).Updates(&userKey).Error; err != nil {
return err
}
}
@@ -384,16 +381,16 @@ func DBInit(db *gorm.DB) error {
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"` // FIXME: replace with hostKey ?
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Host{}).Error
},
@@ -405,14 +402,14 @@ func DBInit(db *gorm.DB) error {
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *User `gorm:"ForeignKey:UserID"`
Host *Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Session{}).Error
},
@@ -422,14 +419,14 @@ func DBInit(db *gorm.DB) error {
}, {
ID: "27",
Migrate: func(tx *gorm.DB) error {
var sessions []*dbmodels.Session
var sessions []Session
if err := db.Find(&sessions).Error; err != nil {
return err
}
for _, session := range sessions {
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
if err := db.Model(session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
if err := db.Model(&session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
return err
}
}
@@ -450,10 +447,10 @@ func DBInit(db *gorm.DB) error {
User string
Password string
URL string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
}
return tx.AutoMigrate(&Host{}).Error
@@ -472,12 +469,12 @@ func DBInit(db *gorm.DB) error {
User string
Password string
URL string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
Hop *dbmodels.Host
Hop *Host
HopID uint
}
return tx.AutoMigrate(&Host{}).Error
@@ -490,7 +487,7 @@ func DBInit(db *gorm.DB) error {
if err := m.Migrate(); err != nil {
return err
}
dbmodels.NewEvent("system", "migrated").Log(db)
NewEvent("system", "migrated").Log(db)
// create default ssh key
var count uint
@@ -498,7 +495,7 @@ func DBInit(db *gorm.DB) error {
return err
}
if count == 0 {
key, err := crypto.NewSSHKey("rsa", 2048)
key, err := NewSSHKey("rsa", 2048)
if err != nil {
return err
}
@@ -514,7 +511,7 @@ func DBInit(db *gorm.DB) error {
return err
}
if count == 0 {
hostGroup := dbmodels.HostGroup{
hostGroup := HostGroup{
Name: "default",
Comment: "created by sshportal",
}
@@ -528,7 +525,7 @@ func DBInit(db *gorm.DB) error {
return err
}
if count == 0 {
userGroup := dbmodels.UserGroup{
userGroup := UserGroup{
Name: "default",
Comment: "created by sshportal",
}
@@ -542,13 +539,13 @@ func DBInit(db *gorm.DB) error {
return err
}
if count == 0 {
var defaultUserGroup dbmodels.UserGroup
var defaultUserGroup UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
var defaultHostGroup dbmodels.HostGroup
var defaultHostGroup HostGroup
db.Where("name = ?", "default").First(&defaultHostGroup)
acl := dbmodels.ACL{
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
acl := ACL{
UserGroups: []*UserGroup{&defaultUserGroup},
HostGroups: []*HostGroup{&defaultHostGroup},
Action: "allow",
//HostPattern: "",
//Weight: 0,
@@ -560,7 +557,7 @@ func DBInit(db *gorm.DB) error {
}
// create admin user
var defaultUserGroup dbmodels.UserGroup
var defaultUserGroup UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
if err := db.Table("users").Count(&count).Error; err != nil {
return err
@@ -571,7 +568,7 @@ func DBInit(db *gorm.DB) error {
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
}
var adminRole dbmodels.UserRole
var adminRole UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
@@ -586,13 +583,13 @@ func DBInit(db *gorm.DB) error {
if username == "" {
username = "admin" // fallback username
}
user := dbmodels.User{
user := User{
Name: username,
Email: fmt.Sprintf("%s@localhost", username),
Comment: "created by sshportal",
Roles: []*dbmodels.UserRole{&adminRole},
Roles: []*UserRole{&adminRole},
InviteToken: inviteToken,
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
Groups: []*UserGroup{&defaultUserGroup},
}
if err := db.Create(&user).Error; err != nil {
return err
@@ -605,7 +602,7 @@ func DBInit(db *gorm.DB) error {
return err
}
if count == 0 {
key, err := crypto.NewSSHKey("rsa", 2048)
key, err := NewSSHKey("rsa", 2048)
if err != nil {
return err
}
@@ -617,8 +614,8 @@ func DBInit(db *gorm.DB) error {
}
// close unclosed connections
return db.Table("sessions").Where("status = ?", "active").Updates(&dbmodels.Session{
Status: string(dbmodels.SessionStatusClosed),
return db.Table("sessions").Where("status = ?", "active").Updates(&Session{
Status: string(SessionStatusClosed),
ErrMsg: "sshportal was halted while the connection was still active",
}).Error
}
@@ -646,13 +643,3 @@ func addExtraSpaceIfExist(str string) string {
}
return ""
}
func randStringBytes(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

View File

@@ -5,6 +5,3 @@ run:
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
docker-compose run client /integration/_client.sh
docker-compose down
build:
docker-compose build

View File

@@ -28,7 +28,7 @@ ssh sshportal -l invite:integration
ssh sshportal -l admin hostgroup create
ssh sshportal -l admin hostgroup create --name=hg1
ssh sshportal -l admin hostgroup create --name=hg2 --comment=test
ssh sshportal -l admin hostgroup inspect hg1 hg2
ssh sshportal -l admin usergroup inspect hg1 hg2
ssh sshportal -l admin hostgroup ls
ssh sshportal -l admin usergroup create

60
go.mod
View File

@@ -1,33 +1,47 @@
module moul.io/sshportal
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/creack/pty v1.1.11 // indirect
cloud.google.com/go v0.33.1 // indirect
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v1.13.1
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect
github.com/docker/docker v1.13.1 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/gliderlabs/ssh v0.3.0
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.1.1 // indirect
github.com/go-gormigrate/gormigrate v1.2.1
github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jinzhu/gorm v1.9.14
github.com/kr/pty v1.1.8
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/jinzhu/gorm v1.9.1
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kr/pty v1.1.3
github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
github.com/olekukonko/tablewriter v0.0.4
github.com/reiver/go-oi v1.0.0
github.com/moby/moby v1.13.1
github.com/moul/ssh v0.1.1-0.20181116135657-8b3cdd49b6d2
github.com/olekukonko/tablewriter v0.0.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/reiver/go-oi v0.0.0-20160325061615-431c83978379
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
github.com/smartystreets/goconvey v1.6.4
github.com/urfave/cli v1.22.4
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
gopkg.in/gormigrate.v1 v1.6.0
moul.io/srand v1.4.0
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c
github.com/spf13/cobra v0.0.3 // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/urfave/cli v1.20.0
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
)
go 1.14

161
go.sum
View File

@@ -1,137 +1,100 @@
cloud.google.com/go v0.33.1 h1:fmJQWZ1w9PGkHR1YL/P7HloDvqlmKQ4Vpb7PC2e+aCk=
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1 h1:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/gliderlabs/ssh v0.3.0 h1:7GcKy4erEljCE/QeQ2jTVpu+3f3zkpZOxOJjFYkMqYU=
github.com/gliderlabs/ssh v0.3.0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-gormigrate/gormigrate v1.2.1 h1:y3jmLDVVxVkuIR4CR5Qu+lLiUUOtpGt+4zjkLH53Bls=
github.com/go-gormigrate/gormigrate v1.2.1/go.mod h1:EmaYTk8H9TxcUD9nFzNPaHlDUCePc1EstS+HTwcGNhE=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 h1:xvj06l8iSwiWpYgm8MbPp+naBg+pwfqmdXabzqPCn/8=
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-colorable v0.0.0-20171111065953-6fcc0c1fd9b6 h1:G4Z3Qt5LMB7t8O2mvgRGe5Napynl/AXz+kEPvYXaggQ=
github.com/mattn/go-colorable v0.0.0-20171111065953-6fcc0c1fd9b6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/moby/moby v0.0.0-20171102073902-76531ccdeb58 h1:ce/WsOd8CTi+SX+mtZolkjdHRFh4WSqqV9pnedmqY1w=
github.com/moby/moby v0.0.0-20171102073902-76531ccdeb58/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/moby v1.13.1 h1:0/u5wsRE+Nb2D9TFTUA8PJxkDaneSYyID4ar9BfMoZM=
github.com/moby/moby v1.13.1/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moul/ssh v0.1.1-0.20181116134500-51417a721208 h1:Y97oa5mCq1XZ+46noGJySDjs6Kf8iY0FqfEa4wPutdc=
github.com/moul/ssh v0.1.1-0.20181116134500-51417a721208/go.mod h1:7g1Z1WW1l5W9MgjgsE6ehNzvjmA8qe9kJ/G8kdanYEg=
github.com/moul/ssh v0.1.1-0.20181116135657-8b3cdd49b6d2 h1:IAH3/wuCKXdfGf4zrH2PtTnp0PhWtL+Cld840EfLQ5o=
github.com/moul/ssh v0.1.1-0.20181116135657-8b3cdd49b6d2/go.mod h1:7g1Z1WW1l5W9MgjgsE6ehNzvjmA8qe9kJ/G8kdanYEg=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-oi v0.0.0-20160325061615-431c83978379 h1:NBPkf14RzPYmr3478XQcmQyMKkxSvguL7+cyKKNvGxY=
github.com/reiver/go-oi v0.0.0-20160325061615-431c83978379/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0=
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/urfave/cli v0.0.0-20171031025534-7f4b273a0585 h1:fKnLpe72GC+2GbMpMp0AmcqVvJGW5GBaWD5C2gomMEg=
github.com/urfave/cli v0.0.0-20171031025534-7f4b273a0585/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18 h1:Wh+XCfg3kNpjhdq2LXrsiOProjtQZKme5XUx7VcxwAw=
golang.org/x/sys v0.0.0-20181116161606-93218def8b18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
moul.io/srand v1.4.0 h1:r5ZMiWDN0ni0lTV7KzJR/jx0K7GivJYW5WaXmufgeik=
moul.io/srand v1.4.0/go.mod h1:P2uaZB+GFstFNo8sEj6/U8FRV1n25kD0LLckFpJ+qvc=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=

View File

@@ -1,22 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,21 +0,0 @@
apiVersion: v2
name: sshportal
description: A Helm chart for SSHPortal on Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: 1.10.0

View File

@@ -1,33 +0,0 @@
1. Get the admin invitation token (only on first install):
export INVITE=$(kubectl --namespace sshportal logs -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" | grep -Eo "invite:[a-zA-Z0-9]+")
2. Get the service IP and Port:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sshportal.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sshportal.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sshportal.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 2222:{{ .Values.service.port }}
{{- end }}
3. Enroll your SSH public key:
{{- if contains "NodePort" .Values.service.type }}
ssh $NODE_IP -p $NODE_PORT -l $INVITE
{{- else if contains "LoadBalancer" .Values.service.type }}
ssh $SERVICE_IP -p {{ .Values.service.port }} -l $INVITE
{{- else if contains "ClusterIP" .Values.service.type }}
ssh localhost -p 2222 -l $INVITE
{{- end }}
4. Configure your {{ include "sshportal.name" . }} install:
{{- if contains "NodePort" .Values.service.type }}
ssh admin@$NODE_IP -p $NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
ssh admin@$SERVICE_IP -p {{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
ssh admin@localhost -p 2222
{{- end }}

View File

@@ -1,63 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "sshportal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "sshportal.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "sshportal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "sshportal.labels" -}}
helm.sh/chart: {{ include "sshportal.chart" . }}
{{ include "sshportal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "sshportal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sshportal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "sshportal.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "sshportal.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@@ -1,69 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sshportal.fullname" . }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sshportal.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "sshportal.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:v{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: ssh
containerPort: 2222
protocol: TCP
livenessProbe:
exec:
command:
- sshportal
- healthcheck
- --quiet
readinessProbe:
exec:
command:
- sshportal
- healthcheck
- --quiet
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if .Values.mysql.enabled }}
- name: SSHPORTAL_DATABASE_URL
value: {{ .Values.mysql.user }}:{{ .Values.mysql.password }}@tcp({{ .Values.mysql.server }}:{{ .Values.mysql.port }})/{{ .Values.mysql.database }}?charset=utf8&parseTime=true&loc=Local
- name: SSHPORTAL_DB_DRIVER
value: mysql
{{- end }}
{{- if .Values.debug}}
- name: SSHPORTAL_DEBUG
value: "1"
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -1,21 +0,0 @@
{{- if .Values.mysql.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "sshportal.fullname" . }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "sshportal.fullname" . }}
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.cpuTarget }}
{{- end }}

View File

@@ -1,17 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "sshportal.fullname" . }}
annotations:
{{- toYaml .Values.service.annotations | nindent 4 }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: 2222
protocol: TCP
name: ssh
selector:
{{- include "sshportal.selectorLabels" . | nindent 4 }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "sshportal.fullname" . }}-test-connection"
labels:
{{ include "sshportal.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "sshportal.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -1,119 +0,0 @@
# Default values for sshportal.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
## Enable SSHPortal debug mode
##
debug: false
## SSH Portal Docker image
##
image:
repository: moul/sshportal
pullPolicy: IfNotPresent
## Reference to one or more secrets to be used when pulling images
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
##
imagePullSecrets: []
## Provide a name in place of sshportal for `app:` labels
##
nameOverride: ""
## Provide a name to substitute for the full names of resources
##
fullnameOverride: ""
## PodSecurityContext holds pod-level security attributes.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
##
podSecurityContext: {}
# fsGroup: 2000
## SecurityContext holds container-level security attributes.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
##
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
## Service
##
service:
## Configure additional annotations for SSHPortal service
##
annotations: {}
# service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
## Service type, one of
## NodePort, ClusterIP, LoadBalancer
##
type: LoadBalancer
## Port to expose on the service
##
port: 22
## Define resources requests and limits
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
##
resources: {}
# requests:
# cpu: 100m
# memory: 128Mi
# limits:
# cpu: 2
# memory: 2Gi
## Mysql/MariaDB configuration for HA
##
mysql:
enabled: false
## Database user
##
user: sshportal
## Database password
##
password: change_me
## Database name
##
database: sshportal
## Database server FQDN or IP
##
server: mariadb-mariadb-galera
## Database port
##
port: 3306
## Define which Nodes the Pods are scheduled on.
## ref: https://kubernetes.io/docs/user-guide/node-selection/
##
nodeSelector: {}
## The pod's tolerations.
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## Assign custom affinity rules
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
##
affinity: {}
## HPA support, require `mysql.enable: true`
## This section enables sshportal to autoscale based on metrics.
##
autoscaling:
maxReplicas: 4
minReplicas: 2
cpuTarget: 60

View File

@@ -11,8 +11,8 @@ import (
"syscall"
"unsafe"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
"github.com/moul/ssh"
"github.com/urfave/cli"
)

101
main.go
View File

@@ -1,31 +1,41 @@
package main // import "moul.io/sshportal"
import (
"fmt"
"log"
"math"
"math/rand"
"net"
"os"
"path"
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/moul/ssh"
"github.com/urfave/cli"
"moul.io/srand"
gossh "golang.org/x/crypto/ssh"
)
var (
// Version should be updated by hand at each release
Version = "1.9.0+dev"
// GitTag will be overwritten automatically by the build system
GitTag = "n/a"
GitTag string
// GitSha will be overwritten automatically by the build system
GitSha = "n/a"
GitSha string
// GitBranch will be overwritten automatically by the build system
GitBranch string
)
func main() {
rand.Seed(srand.Secure())
rand.Seed(time.Now().UnixNano())
app := cli.NewApp()
app.Name = path.Base(os.Args[0])
app.Author = "Manfred Touron"
app.Version = GitTag + " (" + GitSha + ")"
app.Version = Version + " (" + GitSha + ")"
app.Email = "https://moul.io/sshportal"
app.Commands = []cli.Command{
{
@@ -35,7 +45,7 @@ func main() {
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
return err
}
cfg, err := parseServerConfig(c)
cfg, err := parseServeConfig(c)
if err != nil {
return err
}
@@ -110,3 +120,82 @@ func main() {
log.Fatalf("error: %v", err)
}
}
var defaultChannelHandler ssh.ChannelHandler
func server(c *configServe) (err error) {
var db = (*gorm.DB)(nil)
// try to setup the local DB
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
return
}
defer func() {
origErr := err
err = db.Close()
if origErr != nil {
err = origErr
}
}()
if err = db.DB().Ping(); err != nil {
return
}
db.LogMode(c.debug)
if err = dbInit(db); err != nil {
return
}
// create TCP listening socket
ln, err := net.Listen("tcp", c.bindAddr)
if err != nil {
return err
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,
Handler: shellHandler, // ssh.Server.Handler is the handler for the DefaultSessionHandler
Version: fmt.Sprintf("sshportal-%s", Version),
}
// configure channel handler
defaultSessionHandler := srv.GetChannelHandler("session")
defaultDirectTcpipHandler := srv.GetChannelHandler("direct-tcpip")
defaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
go defaultSessionHandler(srv, conn, newChan, ctx)
case "direct-tcpip":
go defaultDirectTcpipHandler(srv, conn, newChan, ctx)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
}
}
srv.SetChannelHandler("session", nil)
srv.SetChannelHandler("direct-tcpip", nil)
srv.SetChannelHandler("default", channelHandler)
if c.idleTimeout != 0 {
srv.IdleTimeout = c.idleTimeout
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
// So, set it to the max value, because we don't want a max timeout.
srv.MaxTimeout = math.MaxInt64
}
for _, opt := range []ssh.Option{
// custom PublicKeyAuth handler
ssh.PublicKeyAuth(publicKeyAuthHandler(db, c)),
ssh.PasswordAuth(passwordAuthHandler(db, c)),
// retrieve sshportal SSH private key from database
privateKeyFromDB(db, c.aesKey),
} {
if err := srv.SetOption(opt); err != nil {
return err
}
}
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
return srv.Serve(ln)
}

View File

@@ -1,44 +0,0 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"sort"
"moul.io/sshportal/pkg/dbmodels"
)
type byWeight []*dbmodels.ACL
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func checkACLs(user dbmodels.User, host dbmodels.Host) string {
// shared ACLs between user and host
aclMap := map[uint]*dbmodels.ACL{}
for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups {
for _, hostGroupACL := range hostGroup.ACLs {
if userGroupACL.ID == hostGroupACL.ID {
aclMap[userGroupACL.ID] = userGroupACL
}
}
}
}
}
// FIXME: add ACLs that match host pattern
// deny by default if no shared ACL
if len(aclMap) == 0 {
return string(dbmodels.ACLActionDeny) // default action
}
// transform map to slice and sort it
acls := make([]*dbmodels.ACL, 0, len(aclMap))
for _, acl := range aclMap {
acls = append(acls, acl)
}
sort.Sort(byWeight(acls))
return acls[0].Action
}

View File

@@ -1,49 +0,0 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
. "github.com/smartystreets/goconvey/convey"
"moul.io/sshportal/pkg/dbmodels"
)
func TestCheckACLs(t *testing.T) {
Convey("Testing CheckACLs", t, func(c C) {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
c.So(err, ShouldBeNil)
defer func() {
c.So(os.RemoveAll(tempDir), ShouldBeNil)
}()
// create sqlite db
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
c.So(err, ShouldBeNil)
db.LogMode(false)
c.So(DBInit(db), ShouldBeNil)
// create dummy objects
var hostGroup dbmodels.HostGroup
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
c.So(err, ShouldBeNil)
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
//. load db
var (
hosts []dbmodels.Host
users []dbmodels.User
)
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action := checkACLs(users[0], hosts[0])
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
})
}

View File

@@ -1,25 +1,34 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
package bastionsession // import "moul.io/sshportal/pkg/bastionsession"
import (
"fmt"
"errors"
"io"
"log"
"os"
"strings"
"time"
"github.com/gliderlabs/ssh"
"github.com/moul/ssh"
"github.com/sabban/bastion/pkg/logchannel"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/logtunnel"
)
type sessionConfig struct {
type ForwardData struct {
DestinationHost string
DestinationPort uint32
SourceHost string
SourcePort uint32
}
type Config struct {
Addr string
Logs string
ClientConfig *gossh.ClientConfig
}
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) error {
var lastClient *gossh.Client
switch newChan.ChannelType() {
case "session":
@@ -47,7 +56,6 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close() // fix #56
return err
}
defer func() { _ = client.Close() }()
@@ -59,10 +67,8 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
case "direct-tcpip":
lch, lreqs, err := newChan.Accept()
// TODO: defer clean closer
@@ -88,14 +94,13 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close()
return err
}
defer func() { _ = client.Close() }()
lastClient = client
}
d := logTunnelForwardData{}
d := logtunnel.ForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
@@ -104,10 +109,8 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
@@ -116,18 +119,17 @@ func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx s
}
}
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error {
defer func() {
_ = lch.Close()
_ = rch.Close()
}()
errch := make(chan error, 1)
quit := make(chan string, 1)
channeltype := newChan.ChannelType()
filename := strings.Join([]string{logsLocation, "/", user, "-", username, "-", channeltype, "-", fmt.Sprint(sessionID), "-", time.Now().Format(time.RFC3339)}, "") // get user
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
filename := strings.Join([]string{logsLocation, "/", user, "-", channeltype, "-", time.Now().Format(time.RFC3339)}, "") // get user
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
defer func() {
_ = f.Close()
}()
@@ -139,36 +141,40 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
log.Printf("Session %v is recorded in %v", channeltype, filename)
if channeltype == "session" {
wrappedlch := logchannel.New(lch, f)
go func(quit chan string) {
go func() {
_, _ = io.Copy(wrappedlch, rch)
quit <- "rch"
}(quit)
errch <- errors.New("lch closed the connection")
}()
go func(quit chan string) {
go func() {
_, _ = io.Copy(rch, lch)
quit <- "lch"
}(quit)
errch <- errors.New("rch closed the connection")
}()
}
if channeltype == "direct-tcpip" {
d := logTunnelForwardData{}
d := logtunnel.ForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
wrappedlch := newLogTunnel(lch, f, d.SourceHost)
wrappedrch := newLogTunnel(rch, f, d.DestinationHost)
go func(quit chan string) {
wrappedlch := logtunnel.New(lch, f, d.SourceHost)
wrappedrch := logtunnel.New(rch, f, d.DestinationHost)
go func() {
_, _ = io.Copy(wrappedlch, rch)
quit <- "rch"
}(quit)
errch <- errors.New("lch closed the connection")
}()
go func(quit chan string) {
go func() {
_, _ = io.Copy(wrappedrch, lch)
quit <- "lch"
}(quit)
errch <- errors.New("rch closed the connection")
}()
}
go func(quit chan string) {
for req := range lreqs {
for {
select {
case req := <-lreqs: // forward ssh requests from local to remote
if req == nil {
return nil
}
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
if req.Type == "exec" {
wrappedlch := logchannel.New(lch, f)
@@ -179,58 +185,24 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
}
if err != nil {
errch <- err
return err
}
if err2 := req.Reply(b, nil); err2 != nil {
errch <- err2
return err2
}
}
quit <- "lreqs"
}(quit)
go func(quit chan string) {
for req := range rreqs {
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
if err != nil {
errch <- err
}
if err2 := req.Reply(b, nil); err2 != nil {
errch <- err2
}
}
quit <- "rreqs"
}(quit)
lchEOF, rchEOF, lchClosed, rchClosed := false, false, false, false
for {
select {
case err := <-errch:
return err
case q := <-quit:
switch q {
case "lch":
lchEOF = true
_ = rch.CloseWrite()
case "rch":
rchEOF = true
_ = lch.CloseWrite()
case "lreqs":
lchClosed = true
case "rreqs":
rchClosed = true
}
if lchEOF && lchClosed && !rchClosed {
rch.Close()
}
if rchEOF && rchClosed && !lchClosed {
lch.Close()
}
if lchEOF && rchEOF && lchClosed && rchClosed {
case req := <-rreqs: // forward ssh requests from remote to local
if req == nil {
return nil
}
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
if err != nil {
return err
}
if err2 := req.Reply(b, nil); err2 != nil {
return err2
}
case err := <-errch:
return err
}
}
}

View File

@@ -1,19 +0,0 @@
package dbmodels
import (
"regexp"
"github.com/asaskevich/govalidator"
)
func InitValidator() {
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
name, ok := i.(string)
if !ok {
return false
}
return unixUserRegexp.MatchString(name)
}))
}

View File

@@ -1,4 +1,5 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
eeeeeeeeepacka,ge logtunnel //
import "moul.io/sshportal/pkg/logtunnel"
import (
"encoding/binary"
@@ -17,7 +18,7 @@ type logTunnel struct {
writer io.WriteCloser
}
type logTunnelForwardData struct {
type ForwardData struct {
DestinationHost string
DestinationPort uint32
SourceHost string
@@ -27,9 +28,10 @@ type logTunnelForwardData struct {
func writeHeader(fd io.Writer, length int) {
t := time.Now()
tv := syscall.NsecToTimeval(t.UnixNano())
if err := binary.Write(fd, binary.LittleEndian, int32(tv.Sec)); err != nil {
tv := syscall.NsecToTimeval,.
(t.
UnixNano()) if err := binary.Write(fd, binary.LittleEndian, int32(tv.Se,
c)); err nil ),
log.Printf("failed to write log header: %v", err)
}
if err := binary.Write(fd, binary.LittleEndian, tv.Usec); err != nil {
@@ -40,32 +42,50 @@ func writeHeader(fd io.Writer, length int) {
}
}
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
func New(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
return &logTunnel{
host: host,
channel: channel,
writer: writer,
}
}
hannel: channel,
,
writer: writer, }},
func (l *logTunnel) Read(data []byte) (int, error) {
return 0, errors.New("logTunnel.Read is not implemented")
}
func (l *logTunnel) Write(data []byte) (int, error) {
func (l *logTunnel) Write(data []byte) (
int, error) {
writeHeader(l.writer, len(data)+len(l.host+": "))
if _, err := l.writer.Write([]byte(l.host + ": ")); err != nil {
if _, err ,
:= l.writer.Write([]byte(l.host + ": ")); err != nil {
log.Printf("failed to write log: %v", err)
}
if _, err := l.writer.Write(data); err != nil {
log.Printf("failed to write log: %v", err)
log.Printf("failed to write log: %v",e err,
)
}
return l.channel.Write(data)
return l.channel.Write(
data),
}
func (l *logTunnel) Close() error {
l.writer.Close()
func (l, *logTunnel) Close,() error { l.writer.Close return
return l.channel.Close()
le.
channel.
Close()
}
,
,
eeeeeee,
eeeeeeeeeeeeeee,ee
,eeeeeeee
,
eeeeee,
eeeee,
eeeeee,

View File

@@ -2,6 +2,5 @@
"extends": [
"config:base"
],
"groupName": "all",
"gomodTidy": true
"groupName": "all"
}

278
rules.mk
View File

@@ -1,278 +0,0 @@
# +--------------------------------------------------------------+
# | * * * moul.io/rules.mk |
# +--------------------------------------------------------------+
# | |
# | ++ ______________________________________ |
# | ++++ / \ |
# | ++++ | | |
# | ++++++++++ | https://moul.io/rules.mk is a set | |
# | +++ | | of common Makefile rules that can | |
# | ++ | | be configured from the Makefile | |
# | + -== ==| | or with environment variables. | |
# | ( <*> <*> | | |
# | | | /| Manfred Touron | |
# | | _) / | manfred.life | |
# | | +++ / \______________________________________/ |
# | \ =+ / |
# | \ + |
# | |\++++++ |
# | | ++++ ||// |
# | ___| |___ _||/__ __|
# | / --- \ \| ||| __ _ ___ __ __/ /|
# |/ | | \ \ / / ' \/ _ \/ // / / |
# || | | | | | /_/_/_/\___/\_,_/_/ |
# +--------------------------------------------------------------+
all: help
##
## Common helpers
##
rwildcard = $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
##
## rules.mk
##
ifneq ($(wildcard rules.mk),)
.PHONY: rulesmk.bumpdeps
rulesmk.bumpdeps:
wget -O rules.mk https://raw.githubusercontent.com/moul/rules.mk/master/rules.mk
BUMPDEPS_STEPS += rulesmk.bumpdeps
endif
##
## Maintainer
##
ifneq ($(wildcard .git/HEAD),)
.PHONY: generate.authors
generate.authors: AUTHORS
AUTHORS: .git/
echo "# This file lists all individuals having contributed content to the repository." > AUTHORS
echo "# For how it is generated, see 'https://github.com/moul/rules.mk'" >> AUTHORS
echo >> AUTHORS
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf >> AUTHORS
GENERATE_STEPS += generate.authors
endif
##
## Golang
##
ifndef GOPKG
ifneq ($(wildcard go.mod),)
GOPKG = $(shell sed '/module/!d;s/^omdule\ //' go.mod)
endif
endif
ifdef GOPKG
GO ?= go
GOPATH ?= $(HOME)/go
GO_INSTALL_OPTS ?=
GO_TEST_OPTS ?= -test.timeout=30s
GOMOD_DIR ?= .
GOCOVERAGE_FILE ?= ./coverage.txt
ifdef GOBINS
.PHONY: go.install
go.install:
@set -e; for dir in $(GOBINS); do ( set -xe; \
cd $$dir; \
$(GO) install $(GO_INSTALL_OPTS) .; \
); done
INSTALL_STEPS += go.install
.PHONY: go.release
go.release:
goreleaser --snapshot --skip-publish --rm-dist
@echo -n "Do you want to release? [y/N] " && read ans && \
if [ $${ans:-N} = y ]; then set -xe; goreleaser --rm-dist; fi
RELEASE_STEPS += go.release
endif
.PHONY: go.unittest
go.unittest:
@echo "mode: atomic" > /tmp/gocoverage
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do (set -e; (set -xe; \
cd $$dir; \
$(GO) test $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race ./...); \
if [ -f /tmp/profile.out ]; then \
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
rm -f /tmp/profile.out; \
fi); done
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
.PHONY: go.checkdoc
go.checkdoc:
go doc $(GOMOD_DIR)
.PHONY: go.coverfunc
go.coverfunc: go.unittest
go tool cover -func=$(GOCOVERAGE_FILE) | grep -v .pb.go: | grep -v .pb.gw.go:
.PHONY: go.lint
go.lint:
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
cd $$dir; \
golangci-lint run --verbose ./...; \
); done
.PHONY: go.tidy
go.tidy:
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
cd $$dir; \
$(GO) mod tidy; \
); done
.PHONY: go.build
go.build:
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
cd $$dir; \
$(GO) build ./...; \
); done
.PHONY: go.bump-deps
go.bumpdeps:
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
cd $$dir; \
$(GO) get -u ./...; \
); done
.PHONY: go.bump-deps
go.fmt:
if ! command -v goimports &>/dev/null; then GO111MODULE=off go get golang.org/x/tools/cmd/goimports; fi
@set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do ( set -xe; \
cd $$dir; \
goimports -w `go list -f '{{.Dir}}' ./...)` \
); done
BUILD_STEPS += go.build
BUMPDEPS_STEPS += go.bumpdeps
TIDY_STEPS += go.tidy
LINT_STEPS += go.lint
UNITTEST_STEPS += go.unittest
FMT_STEPS += go.fmt
endif
##
## Node
##
ifndef NPM_PACKAGES
ifneq ($(wildcard package.json),)
NPM_PACKAGES = .
endif
endif
ifdef NPM_PACKAGES
.PHONY: npm.publish
npm.publish:
@echo -n "Do you want to npm publish? [y/N] " && read ans && \
@if [ $${ans:-N} = y ]; then \
set -e; for dir in $(NPM_PACKAGES); do ( set -xe; \
cd $$dir; \
npm publish --access=public; \
); done; \
fi
RELEASE_STEPS += npm.publish
endif
##
## Docker
##
docker_build = docker build \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VERSION=`git describe --tags --always` \
-t "$2" -f "$1" "$(dir $1)"
ifndef DOCKERFILE_PATH
DOCKERFILE_PATH = ./Dockerfile
endif
ifndef DOCKER_IMAGE
ifneq ($(wildcard Dockerfile),)
DOCKER_IMAGE = $(notdir $(PWD))
endif
endif
ifdef DOCKER_IMAGE
ifneq ($(DOCKER_IMAGE),none)
.PHONY: docker.build
docker.build:
$(call docker_build,$(DOCKERFILE_PATH),$(DOCKER_IMAGE))
BUILD_STEPS += docker.build
endif
endif
##
## Common
##
TEST_STEPS += $(UNITTEST_STEPS)
TEST_STEPS += $(LINT_STEPS)
TEST_STEPS += $(TIDY_STEPS)
ifneq ($(strip $(TEST_STEPS)),)
.PHONY: test
test: $(PRE_TEST_STEPS) $(TEST_STEPS)
endif
ifdef INSTALL_STEPS
.PHONY: install
install: $(PRE_INSTALL_STEPS) $(INSTALL_STEPS)
endif
ifdef UNITTEST_STEPS
.PHONY: unittest
unittest: $(PRE_UNITTEST_STEPS) $(UNITTEST_STEPS)
endif
ifdef LINT_STEPS
.PHONY: lint
lint: $(PRE_LINT_STEPS) $(FMT_STEPS) $(LINT_STEPS)
endif
ifdef TIDY_STEPS
.PHONY: tidy
tidy: $(PRE_TIDY_STEPS) $(TIDY_STEPS)
endif
ifdef BUILD_STEPS
.PHONY: build
build: $(PRE_BUILD_STEPS) $(BUILD_STEPS)
endif
ifdef RELEASE_STEPS
.PHONY: release
release: $(PRE_RELEASE_STEPS) $(RELEASE_STEPS)
endif
ifdef BUMPDEPS_STEPS
.PHONY: bumpdeps
bumpdeps: $(PRE_BUMDEPS_STEPS) $(BUMPDEPS_STEPS)
endif
ifdef FMT_STEPS
.PHONY: fmt
fmt: $(PRE_FMT_STEPS) $(FMT_STEPS)
endif
ifdef GENERATE_STEPS
.PHONY: generate
generate: $(PRE_GENERATE_STEPS) $(GENERATE_STEPS)
endif
.PHONY: help
help::
@echo "General commands:"
@[ "$(BUILD_STEPS)" != "" ] && echo " build" || true
@[ "$(BUMPDEPS_STEPS)" != "" ] && echo " bumpdeps" || true
@[ "$(FMT_STEPS)" != "" ] && echo " fmt" || true
@[ "$(GENERATE_STEPS)" != "" ] && echo " generate" || true
@[ "$(INSTALL_STEPS)" != "" ] && echo " install" || true
@[ "$(LINT_STEPS)" != "" ] && echo " lint" || true
@[ "$(RELEASE_STEPS)" != "" ] && echo " release" || true
@[ "$(TEST_STEPS)" != "" ] && echo " test" || true
@[ "$(TIDY_STEPS)" != "" ] && echo " tidy" || true
@[ "$(UNITTEST_STEPS)" != "" ] && echo " unittest" || true
@# FIXME: list other commands

134
server.go
View File

@@ -1,134 +0,0 @@
package main
import (
"fmt"
"log"
"math"
"net"
"os"
"time"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/bastion"
)
type serverConfig struct {
aesKey string
dbDriver, dbURL string
logsLocation string
bindAddr string
debug, demo bool
idleTimeout time.Duration
}
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
ret := &serverConfig{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
default:
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
return ret, nil
}
func ensureLogDirectory(location string) error {
// check for the logdir existence
logsLocation, err := os.Stat(location)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
}
return err
}
if !logsLocation.IsDir() {
return fmt.Errorf("log directory cannot be created")
}
return nil
}
func server(c *serverConfig) (err error) {
var db = (*gorm.DB)(nil)
// try to setup the local DB
if db, err = gorm.Open(c.dbDriver, c.dbURL); err != nil {
return
}
defer func() {
origErr := err
err = db.Close()
if origErr != nil {
err = origErr
}
}()
if err = db.DB().Ping(); err != nil {
return
}
db.LogMode(c.debug)
if err = bastion.DBInit(db); err != nil {
return
}
// create TCP listening socket
ln, err := net.Listen("tcp", c.bindAddr)
if err != nil {
return err
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,
Handler: func(s ssh.Session) { bastion.ShellHandler(s, GitTag, GitSha, GitTag) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
Version: fmt.Sprintf("sshportal-%s", GitTag),
ChannelHandlers: map[string]ssh.ChannelHandler{
"default": bastion.ChannelHandler,
},
}
// configure channel handler
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
case "direct-tcpip":
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
}
}
if c.idleTimeout != 0 {
srv.IdleTimeout = c.idleTimeout
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
// So, set it to the max value, because we don't want a max timeout.
srv.MaxTimeout = math.MaxInt64
}
for _, opt := range []ssh.Option{
// custom PublicKeyAuth handler
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
// retrieve sshportal SSH private key from database
bastion.PrivateKeyFromDB(db, c.aesKey),
} {
if err := srv.SetOption(opt); err != nil {
return err
}
}
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
return srv.Serve(ln)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
package main
import (
"bytes"
@@ -9,11 +9,11 @@ import (
"strings"
"time"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
"github.com/moul/ssh"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
"moul.io/sshportal/pkg/bastionsession"
)
type sshportalContextKey string
@@ -21,44 +21,40 @@ type sshportalContextKey string
var authContextKey = sshportalContextKey("auth")
type authContext struct {
message string
err error
user dbmodels.User
inputUsername string
db *gorm.DB
userKey dbmodels.UserKey
logsLocation string
aesKey string
dbDriver, dbURL string
bindAddr string
demo, debug bool
authMethod string
authSuccess bool
message string
err error
user User
inputUsername string
db *gorm.DB
userKey UserKey
config *configServe
authMethod string
authSuccess bool
}
type userType string
type UserType string
const (
userTypeHealthcheck userType = "healthcheck"
userTypeBastion userType = "bastion"
userTypeInvite userType = "invite"
userTypeShell userType = "shell"
UserTypeHealthcheck UserType = "healthcheck"
UserTypeBastion UserType = "bastion"
UserTypeInvite UserType = "invite"
UserTypeShell UserType = "shell"
)
func (c authContext) userType() userType {
func (c authContext) userType() UserType {
switch {
case c.inputUsername == "healthcheck":
return userTypeHealthcheck
return UserTypeHealthcheck
case c.inputUsername == c.user.Name || c.inputUsername == c.user.Email || c.inputUsername == "admin":
return userTypeShell
return UserTypeShell
case strings.HasPrefix(c.inputUsername, "invite:"):
return userTypeInvite
return UserTypeInvite
default:
return userTypeBastion
return UserTypeBastion
}
}
func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
if len(host.HostKey) == 0 {
log.Println("Discovering host fingerprint...")
@@ -72,9 +68,7 @@ func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
}
}
var DefaultChannelHandler ssh.ChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {}
func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
case "direct-tcpip":
@@ -89,9 +83,9 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
actx := ctx.Value(authContextKey).(*authContext)
switch actx.userType() {
case userTypeBastion:
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%d,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
host, err := dbmodels.HostByName(actx.db, actx.inputUsername)
case UserTypeBastion:
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
host, err := HostByName(actx.db, actx.inputUsername)
if err != nil {
ch, _, err2 := newChan.Accept()
if err2 != nil {
@@ -104,8 +98,8 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
}
switch host.Scheme() {
case dbmodels.BastionSchemeSSH:
sessionConfigs := make([]sessionConfig, 0)
case BastionSchemeSSH:
sessionConfigs := make([]bastionsession.Config, 0)
currentHost := host
for currentHost != nil {
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
@@ -119,25 +113,25 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
return
}
sessionConfigs = append([]sessionConfig{{
sessionConfigs = append([]bastionsession.Config{{
Addr: currentHost.DialAddr(),
ClientConfig: clientConfig,
Logs: actx.logsLocation,
Logs: actx.config.logsLocation,
}}, sessionConfigs...)
if currentHost.HopID != 0 {
var newHost dbmodels.Host
var newHost Host
actx.db.Model(currentHost).Related(&newHost, "HopID")
hostname := newHost.Name
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
currentHost, _ = HostByName(actx.db, hostname)
} else {
currentHost = nil
}
}
sess := dbmodels.Session{
sess := Session{
UserID: actx.user.ID,
HostID: host.ID,
Status: string(dbmodels.SessionStatusActive),
Status: string(SessionStatusActive),
}
if err = actx.db.Create(&sess).Error; err != nil {
ch, _, err2 := newChan.Accept()
@@ -148,29 +142,31 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
return
}
go func() {
err = multiChannelHandler(conn, newChan, ctx, sessionConfigs, sess.ID)
err = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs)
if err != nil {
log.Printf("Error: %v", err)
}
now := time.Now()
sessUpdate := dbmodels.Session{
Status: string(dbmodels.SessionStatusClosed),
sessUpdate := Session{
Status: string(SessionStatusClosed),
ErrMsg: fmt.Sprintf("%v", err),
StoppedAt: &now,
}
if err == nil {
switch sessUpdate.ErrMsg {
case "lch closed the connection", "rch closed the connection":
sessUpdate.ErrMsg = ""
}
actx.db.Model(&sess).Updates(&sessUpdate)
}()
case dbmodels.BastionSchemeTelnet:
case BastionSchemeTelnet:
tmpSrv := ssh.Server{
// PtyCallback: srv.PtyCallback,
Handler: telnetHandler(host),
}
DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
defaultChannelHandler(&tmpSrv, conn, newChan, ctx)
default:
ch, _, err2 := newChan.Accept()
if err2 != nil {
@@ -181,35 +177,37 @@ func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
_ = ch.Close()
}
default: // shell
DefaultChannelHandler(srv, conn, newChan, ctx)
defaultChannelHandler(srv, conn, newChan, ctx)
}
}
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) {
actx := ctx.Value(authContextKey).(*authContext)
crypto.HostDecrypt(actx.aesKey, host)
crypto.SSHKeyDecrypt(actx.aesKey, host.SSHKey)
clientConfig, err := host.ClientConfig(dynamicHostKey(actx.db, host))
clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host))
if err != nil {
return nil, err
}
var tmpUser dbmodels.User
var tmpUser User
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil {
return nil, err
}
var tmpHost dbmodels.Host
var tmpHost Host
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
return nil, err
}
action, err2 := CheckACLs(tmpUser, tmpHost)
if err2 != nil {
return nil, err2
}
HostDecrypt(actx.config.aesKey, host)
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
action := checkACLs(tmpUser, tmpHost)
switch action {
case string(dbmodels.ACLActionAllow):
// do nothing
case string(dbmodels.ACLActionDeny):
case string(ACLActionAllow):
case string(ACLActionDeny):
return nil, fmt.Errorf("you don't have permission to that host")
default:
return nil, fmt.Errorf("invalid ACL action: %q", action)
@@ -217,10 +215,10 @@ func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientCon
return clientConfig, nil
}
func ShellHandler(s ssh.Session, version, gitSha, gitTag string) {
func shellHandler(s ssh.Session) {
actx := s.Context().Value(authContextKey).(*authContext)
if actx.userType() != userTypeHealthcheck {
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%d,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
if actx.userType() != UserTypeHealthcheck {
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
}
if actx.err != nil {
@@ -234,48 +232,43 @@ func ShellHandler(s ssh.Session, version, gitSha, gitTag string) {
}
switch actx.userType() {
case userTypeHealthcheck:
case UserTypeHealthcheck:
fmt.Fprintln(s, "OK")
return
case userTypeShell:
if err := shell(s, version, gitSha, gitTag); err != nil {
case UserTypeShell:
if err := shell(s); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
_ = s.Exit(1)
}
return
case userTypeInvite:
case UserTypeInvite:
// do nothing (message was printed at the beginning of the function)
return
}
panic("should not happen")
}
func PasswordAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
func passwordAuthHandler(db *gorm.DB, cfg *configServe) ssh.PasswordHandler {
return func(ctx ssh.Context, pass string) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
config: cfg,
authMethod: "password",
}
actx.authSuccess = actx.userType() == userTypeHealthcheck
actx.authSuccess = actx.userType() == UserTypeHealthcheck
ctx.SetValue(authContextKey, actx)
return actx.authSuccess
}
}
func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
func privateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
return func(srv *ssh.Server) error {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
var key SSHKey
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
return err
}
crypto.SSHKeyDecrypt(aesKey, &key)
SSHKeyDecrypt(aesKey, &key)
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
if err != nil {
@@ -286,17 +279,12 @@ func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
}
}
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
config: cfg,
authMethod: "pubkey",
authSuccess: true,
}
@@ -306,20 +294,20 @@ func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bi
db.Where("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&actx.userKey)
if actx.userKey.UserID > 0 {
db.Preload("Roles").Where("id = ?", actx.userKey.UserID).First(&actx.user)
if actx.userType() == userTypeInvite {
if actx.userType() == UserTypeInvite {
actx.err = fmt.Errorf("invites are only supported for new SSH keys; your ssh key is already associated with the user %q", actx.user.Email)
}
return true
}
// handle invite "links"
if actx.userType() == userTypeInvite {
if actx.userType() == UserTypeInvite {
inputToken := strings.Split(actx.inputUsername, ":")[1]
if len(inputToken) > 0 {
db.Where("invite_token = ?", inputToken).First(&actx.user)
}
if actx.user.ID > 0 {
actx.userKey = dbmodels.UserKey{
actx.userKey = UserKey{
UserID: actx.user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
@@ -333,7 +321,7 @@ func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bi
actx.message = fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", actx.user.Name, actx.user.Email)
} else {
actx.user = dbmodels.User{Name: "Anonymous"}
actx.user = User{Name: "Anonymous"}
actx.err = errors.New("your token is invalid or expired")
}
return true
@@ -341,7 +329,7 @@ func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bi
// fallback
actx.err = errors.New("unknown ssh key")
actx.user = dbmodels.User{Name: "Anonymous"}
actx.user = User{Name: "Anonymous"}
return true
}
}

View File

@@ -1,4 +1,4 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
package main
import (
"bufio"
@@ -8,10 +8,9 @@ import (
"log"
"time"
"github.com/gliderlabs/ssh"
"github.com/moul/ssh"
oi "github.com/reiver/go-oi"
telnet "github.com/reiver/go-telnet"
"moul.io/sshportal/pkg/dbmodels"
)
type bastionTelnetCaller struct {
@@ -76,7 +75,7 @@ func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err e
return bufio.ScanLines(data, atEOF)
}
func telnetHandler(host *dbmodels.Host) ssh.Handler {
func telnetHandler(host *Host) ssh.Handler {
return func(s ssh.Session) {
// FIXME: log session in db
//actx := s.Context().Value(authContextKey).(*authContext)

View File

@@ -1,14 +0,0 @@
// +build windows
package main
import (
"fmt"
"github.com/urfave/cli"
)
// testServer is an hidden handler used for integration tests
func testServer(c *cli.Context) error {
return fmt.Errorf("not available on windows")
}

20
util.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import "math/rand"
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func randStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func wrapText(in string, length int) string {
if len(in) <= length {
return in
}
return in[0:length-3] + "..."
}