Compare commits
66 Commits
dev/moul/m
...
v1.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972e232559 | ||
|
|
851a91b1a0 | ||
|
|
6a068dc430 | ||
|
|
2cdfcf60fe | ||
|
|
5d9e0c367a | ||
|
|
cbf8263033 | ||
|
|
846c73d9bc | ||
|
|
e0b43b1976 | ||
|
|
6a6e788968 | ||
|
|
4754cad42a | ||
|
|
db58e53f3b | ||
|
|
b31acb4348 | ||
|
|
c794c2c076 | ||
|
|
42d6cd44bb | ||
|
|
f9057ca56a | ||
|
|
c2f1999037 | ||
|
|
44b386f7a7 | ||
|
|
89b296db4e | ||
|
|
c16403fb3f | ||
|
|
5e21fb72e6 | ||
|
|
c5681bf880 | ||
|
|
db85d6545d | ||
|
|
9912c3deba | ||
|
|
fc5c342e40 | ||
|
|
60707b3faa | ||
|
|
f36845ac6b | ||
|
|
9f76bd6cad | ||
|
|
c53d5d9964 | ||
|
|
171d461ea5 | ||
|
|
b2b04a1155 | ||
|
|
671ba03b78 | ||
|
|
9095725778 | ||
|
|
8b2e5daba3 | ||
|
|
75b7a5f571 | ||
|
|
4b9e881ad0 | ||
|
|
59f8f52cca | ||
|
|
4adaf83fd3 | ||
|
|
84464a4ea6 | ||
|
|
cafac0b8b5 | ||
|
|
5346300a64 | ||
|
|
1d4554eabc | ||
|
|
50bdba8b70 | ||
|
|
8c785f6dea | ||
|
|
93e6abc9ba | ||
|
|
60d7c85c11 | ||
|
|
883bad2ee5 | ||
|
|
7d68e144f8 | ||
|
|
7f32e38cf8 | ||
|
|
43a96d1636 | ||
|
|
00e7d2e45d | ||
|
|
2e711c3591 | ||
|
|
5d147fc03b | ||
|
|
3dccefbbcb | ||
|
|
7c4995fa4a | ||
|
|
2b8f051414 | ||
|
|
4e17c81d63 | ||
|
|
8b4b677d6a | ||
|
|
47229bf473 | ||
|
|
ec5b567da9 | ||
|
|
03b59fae1c | ||
|
|
ede8b3ecf2 | ||
|
|
7ae90b9199 | ||
|
|
a651da451e | ||
|
|
f220af5c54 | ||
|
|
eebf987900 | ||
|
|
3d5101011f |
BIN
.assets/cluster-mysql.png
Normal file
BIN
.assets/cluster-mysql.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -1,12 +1,12 @@
|
||||
digraph {
|
||||
rankdir=LR;
|
||||
layout=dot;
|
||||
node[shape=record];
|
||||
|
||||
start[label="ssh sshportal";color=blue;fontcolor=blue;fontsize=20];
|
||||
|
||||
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];
|
||||
|
||||
subgraph cluster_sshportal {
|
||||
graph[fontsize=20;style=dashed;color=purple;fontcolor=purple];
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
label="sshportal";
|
||||
{
|
||||
node[color=darkorange;fontcolor=darkorange];
|
||||
@@ -17,25 +17,25 @@ digraph {
|
||||
}
|
||||
{
|
||||
node[color=darkgreen;fontcolor=darkgreen];
|
||||
builtin_shell[label="built-in shell"];
|
||||
ssh_proxy[label="SSH proxy"];
|
||||
learn_key[label="learn key"];
|
||||
builtin_shell[label="built-in\nconfig shell"];
|
||||
ssh_proxy[label="SSH proxy\nJump-Host"];
|
||||
learn_key[label="learn the\npub key"];
|
||||
}
|
||||
err_and_exit[label="error and exit";color=red;fontcolor=red];
|
||||
err_and_exit[label="\nerror\nand exit\n\n";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=20;style=dashed;color=purple;fontcolor=purple];
|
||||
graph[fontsize=18;color=gray;fontcolor=black];
|
||||
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[headlabel="user=invite:<token>"];
|
||||
unknown_user_key -> invite_manager[label="user=invite:<token>";labelloc=b];
|
||||
}
|
||||
{
|
||||
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"];
|
||||
invite_manager -> err_and_exit[label="invalid token"];
|
||||
unknown_user_key -> err_and_exit[label="any other user";constraint=false];
|
||||
invite_manager -> err_and_exit[label="invalid token";constraint=false];
|
||||
}
|
||||
}
|
||||
BIN
.assets/flow-diagram.png
Normal file
BIN
.assets/flow-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -4,185 +4,175 @@
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: %3 Pages: 1 -->
|
||||
<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)">
|
||||
<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)">
|
||||
<title>%3</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-308 1022.4219,-308 1022.4219,4 -4,4"/>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-306 1145.8281,-306 1145.8281,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_sshportal</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_hosts</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- start -->
|
||||
<g id="node1" class="node">
|
||||
<title>start</title>
|
||||
<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>
|
||||
<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">$> ssh sshportal</text>
|
||||
</g>
|
||||
<!-- known_user_key -->
|
||||
<g id="node2" class="node">
|
||||
<title>known_user_key</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- start->known_user_key -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>start->known_user_key</title>
|
||||
<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"/>
|
||||
<path fill="none" stroke="#0000ff" d="M158.6917,-156.7092C173.8232,-159.8931 189.4365,-163.1783 203.8727,-166.2158"/>
|
||||
</g>
|
||||
<!-- unknown_user_key -->
|
||||
<g id="node3" class="node">
|
||||
<title>unknown_user_key</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- start->unknown_user_key -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>start->unknown_user_key</title>
|
||||
<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"/>
|
||||
<path fill="none" stroke="#0000ff" d="M142.2895,-121.9827C161.3902,-116.521 182.3703,-110.5218 201.4801,-105.0575"/>
|
||||
</g>
|
||||
<!-- acl_manager -->
|
||||
<g id="node5" class="node">
|
||||
<title>acl_manager</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- known_user_key->acl_manager -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>known_user_key->acl_manager</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- builtin_shell -->
|
||||
<g id="node6" class="node">
|
||||
<title>builtin_shell</title>
|
||||
<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-in shell</text>
|
||||
<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-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>
|
||||
</g>
|
||||
<!-- known_user_key->builtin_shell -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>known_user_key->builtin_shell</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- err_and_exit -->
|
||||
<g id="node9" class="node">
|
||||
<title>err_and_exit</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- known_user_key->err_and_exit -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>known_user_key->err_and_exit</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- invite_manager -->
|
||||
<g id="node4" class="node">
|
||||
<title>invite_manager</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- unknown_user_key->invite_manager -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>unknown_user_key->invite_manager</title>
|
||||
<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:<token></text>
|
||||
<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:<token></text>
|
||||
</g>
|
||||
<!-- unknown_user_key->err_and_exit -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>unknown_user_key->err_and_exit</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- learn_key -->
|
||||
<g id="node8" class="node">
|
||||
<title>learn_key</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- invite_manager->learn_key -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>invite_manager->learn_key</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- invite_manager->err_and_exit -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>invite_manager->err_and_exit</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- ssh_proxy -->
|
||||
<g id="node7" class="node">
|
||||
<title>ssh_proxy</title>
|
||||
<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>
|
||||
<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-Host</text>
|
||||
</g>
|
||||
<!-- acl_manager->ssh_proxy -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>acl_manager->ssh_proxy</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- acl_manager->err_and_exit -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>acl_manager->err_and_exit</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- host_1 -->
|
||||
<g id="node10" class="node">
|
||||
<title>host_1</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>ssh_proxy->host_1</title>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</g>
|
||||
<!-- host_2 -->
|
||||
<g id="node11" class="node">
|
||||
<title>host_2</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>ssh_proxy->host_2</title>
|
||||
<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"/>
|
||||
<path fill="none" stroke="#0000ff" d="M954.1887,-186C966.458,-186 980.0332,-186 993.2463,-186"/>
|
||||
</g>
|
||||
<!-- host_3 -->
|
||||
<g id="node12" class="node">
|
||||
<title>host_3</title>
|
||||
<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>
|
||||
<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>
|
||||
</g>
|
||||
<!-- ssh_proxy->host_3 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>ssh_proxy->host_3</title>
|
||||
<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"/>
|
||||
<path fill="none" stroke="#0000ff" d="M954.1887,-169.6471C971.9014,-163.1558 992.3359,-155.667 1010.4731,-149.0201"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 14 KiB |
BIN
.assets/overview.png
Normal file
BIN
.assets/overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,7 +1,7 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/moul.io/sshportal
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.14.0
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
name: Install Docker Compose
|
||||
command: |
|
||||
umask 022
|
||||
curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
||||
curl -L https://github.com/docker/compose/releases/download/1.11.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
- *install_retry
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# .git/ # should be kept for git-based versionning
|
||||
|
||||
examples/
|
||||
.circleci/
|
||||
.assets/
|
||||
|
||||
6
.github/FUNDING.yml
vendored
Normal file
6
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
github: ["moul"]
|
||||
patreon: moul
|
||||
open_collective: sshportal
|
||||
custom:
|
||||
- "https://www.buymeacoffee.com/moul"
|
||||
- "https://manfred.life/donate"
|
||||
13
.github/workflows/release.yml
vendored
Normal file
13
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
dist/
|
||||
*~
|
||||
*#
|
||||
.*#
|
||||
.DS_Store
|
||||
/log/
|
||||
/sshportal
|
||||
*.db
|
||||
|
||||
29
.goreleaser.yml
Normal file
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
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"
|
||||
8
.releaserc.js
Normal file
8
.releaserc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
branch: 'master',
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/github',
|
||||
],
|
||||
};
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,7 +2,21 @@
|
||||
|
||||
## master (unreleased)
|
||||
|
||||
* Bump deps
|
||||
* 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)
|
||||
|
||||
|
||||
## v1.9.0 (2018-11-18)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# build
|
||||
FROM golang:1.11 as builder
|
||||
COPY . /go/src/moul.io/sshportal
|
||||
FROM golang:1.14.0 as builder
|
||||
ENV GO111MODULE=on
|
||||
WORKDIR /go/src/moul.io/sshportal
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . ./
|
||||
RUN make _docker_install
|
||||
|
||||
# minimal runtime
|
||||
|
||||
11
Makefile
11
Makefile
@@ -45,3 +45,14 @@ 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
|
||||
|
||||
418
README.md
418
README.md
@@ -1,65 +1,43 @@
|
||||
# sshportal
|
||||
|
||||
[](https://circleci.com/gh/moul/sshportal)
|
||||
[](https://hub.docker.com/r/moul/sshportal/)
|
||||
[](https://goreportcard.com/report/moul.io/sshportal)
|
||||
[](https://goreportcard.com/report/moul.io/sshportal)
|
||||
[](https://godoc.org/moul.io/sshportal)
|
||||
[](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://opencollective.com/sshportal) [](https://github.com/moul/sshportal/blob/master/LICENSE)
|
||||
[](https://github.com/moul/sshportal/releases)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_shield)
|
||||
<!-- temporarily broken? [](https://hub.docker.com/r/moul/sshportal/) -->
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
## Contents
|
||||
|
||||

|
||||
<!-- toc -->
|
||||
|
||||
## Features
|
||||
- [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)
|
||||
|
||||
* 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
|
||||
<!-- tocstop -->
|
||||
|
||||
## (Known) limitations
|
||||
---
|
||||
|
||||
* Does not work (yet?) with [`mosh`](https://mosh.org/)
|
||||
|
||||
## Usage
|
||||
## Installation and usage
|
||||
|
||||
Start the server
|
||||
|
||||
@@ -139,9 +117,151 @@ To associate this account with a key, use the following SSH user: 'invite:NfHK5a
|
||||
config>
|
||||
```
|
||||
|
||||
## Flow Diagram
|
||||
Demo gif:
|
||||

|
||||
|
||||

|
||||
---
|
||||
|
||||
## 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
|
||||
* 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/)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## built-in shell
|
||||
|
||||
@@ -157,7 +277,29 @@ ssh admin@portal.example.org host inspect toto
|
||||
|
||||
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
|
||||
|
||||
### Synopsis
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```sh
|
||||
# acl management
|
||||
@@ -230,120 +372,7 @@ 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
|
||||
|
||||
@@ -377,6 +406,33 @@ $ 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.
|
||||
@@ -385,10 +441,12 @@ 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`.
|
||||
|
||||

|
||||

|
||||
|
||||
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)
|
||||
@@ -406,12 +464,34 @@ 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
|
||||
|
||||

|
||||

|
||||
|
||||
## Note
|
||||
## Contributors
|
||||
|
||||
This is totally experimental for now, so please file issues to let me know what you think about it!
|
||||
### Code Contributors
|
||||
|
||||
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>
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large)
|
||||
### 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>
|
||||
|
||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-slate
|
||||
47
acl_test.go
47
acl_test.go
@@ -1,47 +0,0 @@
|
||||
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
52
config.go
@@ -1,52 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -5,3 +5,6 @@ run:
|
||||
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
|
||||
docker-compose run client /integration/_client.sh
|
||||
docker-compose down
|
||||
|
||||
build:
|
||||
docker-compose build
|
||||
|
||||
@@ -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 usergroup inspect hg1 hg2
|
||||
ssh sshportal -l admin hostgroup inspect hg1 hg2
|
||||
ssh sshportal -l admin hostgroup ls
|
||||
|
||||
ssh sshportal -l admin usergroup create
|
||||
|
||||
56
go.mod
56
go.mod
@@ -1,45 +1,35 @@
|
||||
module moul.io/sshportal
|
||||
|
||||
require (
|
||||
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/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/creack/pty v1.1.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect
|
||||
github.com/docker/docker v1.13.1 // indirect
|
||||
github.com/docker/docker v1.13.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
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/gliderlabs/ssh v0.2.2
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // 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/jinzhu/gorm v1.9.12
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||
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/olekukonko/tablewriter v0.0.4
|
||||
github.com/reiver/go-oi v1.0.0
|
||||
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-20180927180507-b2de0cb4f26d // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c
|
||||
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
|
||||
github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4-0.20190330032615-68dc04aab96a
|
||||
github.com/urfave/cli v1.22.2
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867 // indirect
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
moul.io/srand v1.4.0
|
||||
)
|
||||
|
||||
go 1.14
|
||||
|
||||
141
go.sum
141
go.sum
@@ -1,14 +1,24 @@
|
||||
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/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/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/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.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/docker/docker v1.13.1 h1:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M=
|
||||
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/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=
|
||||
@@ -16,81 +26,104 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
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.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
|
||||
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
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/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.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/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/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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/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.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
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 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/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/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/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 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-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-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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
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/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
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/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=
|
||||
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-0.20190330032615-68dc04aab96a h1:XmieTxr5Ejfoo1izsMZO4qWqOTpYagCqNMJyP87ONS0=
|
||||
github.com/smartystreets/goconvey v1.6.4-0.20190330032615-68dc04aab96a/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.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
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-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg=
|
||||
golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
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=
|
||||
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=
|
||||
|
||||
22
helm/sshportal/.helmignore
Normal file
22
helm/sshportal/.helmignore
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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/
|
||||
21
helm/sshportal/Chart.yaml
Normal file
21
helm/sshportal/Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
33
helm/sshportal/templates/NOTES.txt
Normal file
33
helm/sshportal/templates/NOTES.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
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 }}
|
||||
63
helm/sshportal/templates/_helpers.tpl
Normal file
63
helm/sshportal/templates/_helpers.tpl
Normal file
@@ -0,0 +1,63 @@
|
||||
{{/* 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 -}}
|
||||
69
helm/sshportal/templates/deployment.yaml
Normal file
69
helm/sshportal/templates/deployment.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
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 }}
|
||||
21
helm/sshportal/templates/horizontal-pod-autoscaling.yaml
Normal file
21
helm/sshportal/templates/horizontal-pod-autoscaling.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- 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 }}
|
||||
|
||||
17
helm/sshportal/templates/service.yaml
Normal file
17
helm/sshportal/templates/service.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
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 }}
|
||||
15
helm/sshportal/templates/tests/test-connection.yaml
Normal file
15
helm/sshportal/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
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
|
||||
119
helm/sshportal/values.yaml
Normal file
119
helm/sshportal/values.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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
|
||||
93
main.go
93
main.go
@@ -1,26 +1,20 @@
|
||||
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"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/srand"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version should be updated by hand at each release
|
||||
Version = "1.9.0+dev"
|
||||
Version = "1.10.0+dev"
|
||||
// GitTag will be overwritten automatically by the build system
|
||||
GitTag string
|
||||
// GitSha will be overwritten automatically by the build system
|
||||
@@ -30,7 +24,7 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Seed(srand.Secure())
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = path.Base(os.Args[0])
|
||||
@@ -45,7 +39,7 @@ func main() {
|
||||
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := parseServeConfig(c)
|
||||
cfg, err := parseServerConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,82 +114,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package main
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import "sort"
|
||||
import (
|
||||
"sort"
|
||||
|
||||
type ByWeight []*ACL
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
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 }
|
||||
type byWeight []*dbmodels.ACL
|
||||
|
||||
func CheckACLs(user User, host Host) (string, error) {
|
||||
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, error) {
|
||||
// shared ACLs between user and host
|
||||
aclMap := map[uint]*ACL{}
|
||||
aclMap := map[uint]*dbmodels.ACL{}
|
||||
for _, userGroup := range user.Groups {
|
||||
for _, userGroupACL := range userGroup.ACLs {
|
||||
for _, hostGroup := range host.Groups {
|
||||
@@ -26,15 +30,15 @@ func CheckACLs(user User, host Host) (string, error) {
|
||||
|
||||
// deny by default if no shared ACL
|
||||
if len(aclMap) == 0 {
|
||||
return string(ACLActionDeny), nil // default action
|
||||
return string(dbmodels.ACLActionDeny), nil // default action
|
||||
}
|
||||
|
||||
// transform map to slice and sort it
|
||||
acls := make([]*ACL, 0, len(aclMap))
|
||||
acls := make([]*dbmodels.ACL, 0, len(aclMap))
|
||||
for _, acl := range aclMap {
|
||||
acls = append(acls, acl)
|
||||
}
|
||||
sort.Sort(ByWeight(acls))
|
||||
sort.Sort(byWeight(acls))
|
||||
|
||||
return acls[0].Action, nil
|
||||
}
|
||||
50
pkg/bastion/acl_test.go
Normal file
50
pkg/bastion/acl_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
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, err := checkACLs(users[0], hosts[0])
|
||||
c.So(err, ShouldBeNil)
|
||||
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
|
||||
})
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
package main
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
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)
|
||||
@@ -43,9 +46,9 @@ func dbInit(db *gorm.DB) error {
|
||||
Type string
|
||||
Length uint
|
||||
Fingerprint string
|
||||
PrivKey string `sql:"size:10000"`
|
||||
PubKey string `sql:"size:10000"`
|
||||
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
PrivKey string `sql:"size:10000"`
|
||||
PubKey string `sql:"size:10000"`
|
||||
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&SSHKey{}).Error
|
||||
@@ -63,9 +66,9 @@ func dbInit(db *gorm.DB) error {
|
||||
Addr string
|
||||
User string
|
||||
Password string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Fingerprint string
|
||||
Comment string
|
||||
}
|
||||
@@ -79,9 +82,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 *User `gorm:"ForeignKey:UserID"`
|
||||
Key []byte `sql:"size:10000"`
|
||||
UserID uint ``
|
||||
User *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
@@ -98,8 +101,8 @@ func dbInit(db *gorm.DB) error {
|
||||
IsAdmin bool
|
||||
Email string
|
||||
Name string
|
||||
Keys []*UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
|
||||
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
|
||||
Comment string
|
||||
InviteToken string
|
||||
}
|
||||
@@ -114,8 +117,8 @@ func dbInit(db *gorm.DB) error {
|
||||
type UserGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Users []*User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&UserGroup{}).Error
|
||||
@@ -129,8 +132,8 @@ func dbInit(db *gorm.DB) error {
|
||||
type HostGroup struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Hosts []*Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
|
||||
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
|
||||
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&HostGroup{}).Error
|
||||
@@ -143,8 +146,8 @@ func dbInit(db *gorm.DB) error {
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type ACL struct {
|
||||
gorm.Model
|
||||
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
|
||||
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
|
||||
HostPattern string
|
||||
Action string
|
||||
Weight uint
|
||||
@@ -159,64 +162,64 @@ func dbInit(db *gorm.DB) error {
|
||||
}, {
|
||||
ID: "9",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name")
|
||||
return db.Model(&dbmodels.Setting{}).AddUniqueIndex("uix_settings_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
return db.Model(&dbmodels.Setting{}).RemoveIndex("uix_settings_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "10",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name")
|
||||
return db.Model(&dbmodels.SSHKey{}).AddUniqueIndex("uix_keys_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
return db.Model(&dbmodels.SSHKey{}).RemoveIndex("uix_keys_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "11",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name")
|
||||
return db.Model(&dbmodels.Host{}).AddUniqueIndex("uix_hosts_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
return db.Model(&dbmodels.Host{}).RemoveIndex("uix_hosts_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "12",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&User{}).AddUniqueIndex("uix_users_name", "name").Error
|
||||
db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name")
|
||||
return db.Model(&dbmodels.User{}).AddUniqueIndex("uix_users_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&User{}).RemoveIndex("uix_users_name").Error
|
||||
return db.Model(&dbmodels.User{}).RemoveIndex("uix_users_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "13",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name")
|
||||
return db.Model(&dbmodels.UserGroup{}).AddUniqueIndex("uix_usergroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
return db.Model(&dbmodels.UserGroup{}).RemoveIndex("uix_usergroups_name").Error
|
||||
},
|
||||
}, {
|
||||
ID: "14",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
db.Model(&dbmodels.HostGroup{}).RemoveIndex("uix_hostgroups_name")
|
||||
return db.Model(&dbmodels.HostGroup{}).AddUniqueIndex("uix_hostgroups_name", "name").Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return db.Model(&HostGroup{}).RemoveIndex("uix_hostgroups_name").Error
|
||||
return db.Model(&dbmodels.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 []*User `gorm:"many2many:user_user_roles"`
|
||||
Name string `valid:"required,length(1|32),unix_user"`
|
||||
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserRole{}).Error
|
||||
},
|
||||
@@ -229,13 +232,13 @@ func dbInit(db *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
IsAdmin bool
|
||||
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)"`
|
||||
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)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
@@ -245,20 +248,20 @@ func dbInit(db *gorm.DB) error {
|
||||
}, {
|
||||
ID: "17",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&UserRole{Name: "admin"}).Error
|
||||
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "admin").Delete(&UserRole{}).Error
|
||||
return tx.Where("name = ?", "admin").Delete(&dbmodels.UserRole{}).Error
|
||||
},
|
||||
}, {
|
||||
ID: "18",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var adminRole UserRole
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var users []User
|
||||
var users []dbmodels.User
|
||||
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -279,13 +282,13 @@ func dbInit(db *gorm.DB) error {
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type User struct {
|
||||
gorm.Model
|
||||
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)"`
|
||||
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)"`
|
||||
}
|
||||
return tx.AutoMigrate(&User{}).Error
|
||||
},
|
||||
@@ -295,24 +298,24 @@ func dbInit(db *gorm.DB) error {
|
||||
}, {
|
||||
ID: "20",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Create(&UserRole{Name: "listhosts"}).Error
|
||||
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Where("name = ?", "listhosts").Delete(&UserRole{}).Error
|
||||
return tx.Where("name = ?", "listhosts").Delete(&dbmodels.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 *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"`
|
||||
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"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
@@ -324,12 +327,12 @@ func dbInit(db *gorm.DB) error {
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
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)"`
|
||||
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)"`
|
||||
}
|
||||
return tx.AutoMigrate(&Event{}).Error
|
||||
},
|
||||
@@ -341,11 +344,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 *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 *dbmodels.User `gorm:"ForeignKey:UserID"`
|
||||
Comment string `valid:"optional"`
|
||||
}
|
||||
return tx.AutoMigrate(&UserKey{}).Error
|
||||
},
|
||||
@@ -355,7 +358,7 @@ func dbInit(db *gorm.DB) error {
|
||||
}, {
|
||||
ID: "24",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var userKeys []UserKey
|
||||
var userKeys []dbmodels.UserKey
|
||||
if err := db.Find(&userKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -381,16 +384,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 *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"`
|
||||
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"`
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
},
|
||||
@@ -402,14 +405,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 *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"`
|
||||
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"`
|
||||
}
|
||||
return tx.AutoMigrate(&Session{}).Error
|
||||
},
|
||||
@@ -419,7 +422,7 @@ func dbInit(db *gorm.DB) error {
|
||||
}, {
|
||||
ID: "27",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
var sessions []Session
|
||||
var sessions []dbmodels.Session
|
||||
if err := db.Find(&sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -447,10 +450,10 @@ func dbInit(db *gorm.DB) error {
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
@@ -469,12 +472,12 @@ func dbInit(db *gorm.DB) error {
|
||||
User string
|
||||
Password string
|
||||
URL string
|
||||
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
|
||||
SSHKeyID uint `gorm:"index"`
|
||||
HostKey []byte `sql:"size:10000"`
|
||||
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
|
||||
Comment string
|
||||
Hop *Host
|
||||
Hop *dbmodels.Host
|
||||
HopID uint
|
||||
}
|
||||
return tx.AutoMigrate(&Host{}).Error
|
||||
@@ -487,7 +490,7 @@ func dbInit(db *gorm.DB) error {
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
NewEvent("system", "migrated").Log(db)
|
||||
dbmodels.NewEvent("system", "migrated").Log(db)
|
||||
|
||||
// create default ssh key
|
||||
var count uint
|
||||
@@ -495,7 +498,7 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
key, err := crypto.NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -511,7 +514,7 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
hostGroup := HostGroup{
|
||||
hostGroup := dbmodels.HostGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
@@ -525,7 +528,7 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
userGroup := UserGroup{
|
||||
userGroup := dbmodels.UserGroup{
|
||||
Name: "default",
|
||||
Comment: "created by sshportal",
|
||||
}
|
||||
@@ -539,13 +542,13 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
var defaultUserGroup UserGroup
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
var defaultHostGroup HostGroup
|
||||
var defaultHostGroup dbmodels.HostGroup
|
||||
db.Where("name = ?", "default").First(&defaultHostGroup)
|
||||
acl := ACL{
|
||||
UserGroups: []*UserGroup{&defaultUserGroup},
|
||||
HostGroups: []*HostGroup{&defaultHostGroup},
|
||||
acl := dbmodels.ACL{
|
||||
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
|
||||
Action: "allow",
|
||||
//HostPattern: "",
|
||||
//Weight: 0,
|
||||
@@ -557,7 +560,7 @@ func dbInit(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// create admin user
|
||||
var defaultUserGroup UserGroup
|
||||
var defaultUserGroup dbmodels.UserGroup
|
||||
db.Where("name = ?", "default").First(&defaultUserGroup)
|
||||
if err := db.Table("users").Count(&count).Error; err != nil {
|
||||
return err
|
||||
@@ -568,7 +571,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 UserRole
|
||||
var adminRole dbmodels.UserRole
|
||||
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -583,13 +586,13 @@ func dbInit(db *gorm.DB) error {
|
||||
if username == "" {
|
||||
username = "admin" // fallback username
|
||||
}
|
||||
user := User{
|
||||
user := dbmodels.User{
|
||||
Name: username,
|
||||
Email: fmt.Sprintf("%s@localhost", username),
|
||||
Comment: "created by sshportal",
|
||||
Roles: []*UserRole{&adminRole},
|
||||
Roles: []*dbmodels.UserRole{&adminRole},
|
||||
InviteToken: inviteToken,
|
||||
Groups: []*UserGroup{&defaultUserGroup},
|
||||
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return err
|
||||
@@ -602,7 +605,7 @@ func dbInit(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
key, err := NewSSHKey("rsa", 2048)
|
||||
key, err := crypto.NewSSHKey("rsa", 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -614,8 +617,8 @@ func dbInit(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// close unclosed connections
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&Session{
|
||||
Status: string(SessionStatusClosed),
|
||||
return db.Table("sessions").Where("status = ?", "active").Updates(&dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: "sshportal was halted while the connection was still active",
|
||||
}).Error
|
||||
}
|
||||
@@ -643,3 +646,13 @@ 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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package logtunnel // import "moul.io/sshportal/pkg/logtunnel"
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
@@ -17,7 +17,7 @@ type logTunnel struct {
|
||||
writer io.WriteCloser
|
||||
}
|
||||
|
||||
type ForwardData struct {
|
||||
type logTunnelForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
@@ -40,7 +40,7 @@ func writeHeader(fd io.Writer, length int) {
|
||||
}
|
||||
}
|
||||
|
||||
func New(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
|
||||
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
|
||||
return &logTunnel{
|
||||
host: host,
|
||||
channel: channel,
|
||||
@@ -1,34 +1,26 @@
|
||||
package bastionsession // import "moul.io/sshportal/pkg/bastionsession"
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/moul/ssh"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/sabban/bastion/pkg/logchannel"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
|
||||
"moul.io/sshportal/pkg/logtunnel"
|
||||
)
|
||||
|
||||
type ForwardData struct {
|
||||
DestinationHost string
|
||||
DestinationPort uint32
|
||||
SourceHost string
|
||||
SourcePort uint32
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
type sessionConfig struct {
|
||||
Addr string
|
||||
Logs string
|
||||
ClientConfig *gossh.ClientConfig
|
||||
}
|
||||
|
||||
func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) error {
|
||||
func multiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
|
||||
var lastClient *gossh.Client
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
@@ -56,6 +48,7 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close() // fix #56
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
@@ -67,8 +60,10 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
|
||||
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, newChan)
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
|
||||
case "direct-tcpip":
|
||||
lch, lreqs, err := newChan.Accept()
|
||||
// TODO: defer clean closer
|
||||
@@ -94,13 +89,14 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
|
||||
client = gossh.NewClient(ncc, chans, reqs)
|
||||
}
|
||||
if err != nil {
|
||||
lch.Close()
|
||||
return err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
lastClient = client
|
||||
}
|
||||
|
||||
d := logtunnel.ForwardData{}
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,8 +105,10 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
|
||||
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, newChan)
|
||||
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, username, sessionID, newChan)
|
||||
default:
|
||||
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
|
||||
log.Printf("failed to reject chan: %v", err)
|
||||
@@ -119,7 +117,7 @@ func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error {
|
||||
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
|
||||
defer func() {
|
||||
_ = lch.Close()
|
||||
_ = rch.Close()
|
||||
@@ -128,8 +126,8 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
|
||||
errch := make(chan error, 1)
|
||||
channeltype := newChan.ChannelType()
|
||||
|
||||
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)
|
||||
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)
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
@@ -152,12 +150,12 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati
|
||||
}()
|
||||
}
|
||||
if channeltype == "direct-tcpip" {
|
||||
d := logtunnel.ForwardData{}
|
||||
d := logTunnelForwardData{}
|
||||
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
||||
return err
|
||||
}
|
||||
wrappedlch := logtunnel.New(lch, f, d.SourceHost)
|
||||
wrappedrch := logtunnel.New(rch, f, d.DestinationHost)
|
||||
wrappedlch := newLogTunnel(lch, f, d.SourceHost)
|
||||
wrappedrch := newLogTunnel(rch, f, d.DestinationHost)
|
||||
go func() {
|
||||
_, _ = io.Copy(wrappedlch, rch)
|
||||
errch <- errors.New("lch closed the connection")
|
||||
@@ -1,24 +1,29 @@
|
||||
package main
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
shlex "github.com/anmitsu/go-shlex"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/moul/ssh"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/urfave/cli"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
var banner = `
|
||||
@@ -36,7 +41,7 @@ const (
|
||||
naMessage = "n/a"
|
||||
)
|
||||
|
||||
func shell(s ssh.Session) error {
|
||||
func shell(s ssh.Session, version, gitSha, gitTag, gitBranch string) error {
|
||||
var (
|
||||
sshCommand = s.Command()
|
||||
actx = s.Context().Value(authContextKey).(*authContext)
|
||||
@@ -81,35 +86,35 @@ GLOBAL OPTIONS:
|
||||
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns `USERGROUP` to the acl"},
|
||||
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
|
||||
cli.StringFlag{Name: "comment", Usage: "Adds a comment"},
|
||||
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: string(ACLActionAllow)},
|
||||
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: string(dbmodels.ACLActionAllow)},
|
||||
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := myself.CheckRoles([]string{"admin"}); err != nil {
|
||||
return err
|
||||
}
|
||||
acl := ACL{
|
||||
acl := dbmodels.ACL{
|
||||
Comment: c.String("comment"),
|
||||
HostPattern: c.String("pattern"),
|
||||
UserGroups: []*UserGroup{},
|
||||
HostGroups: []*HostGroup{},
|
||||
UserGroups: []*dbmodels.UserGroup{},
|
||||
HostGroups: []*dbmodels.HostGroup{},
|
||||
Weight: c.Uint("weight"),
|
||||
Action: c.String("action"),
|
||||
}
|
||||
if acl.Action != string(ACLActionAllow) && acl.Action != string(ACLActionDeny) {
|
||||
if acl.Action != string(dbmodels.ACLActionAllow) && acl.Action != string(dbmodels.ACLActionDeny) {
|
||||
return fmt.Errorf("invalid action %q, allowed values: allow, deny", acl.Action)
|
||||
}
|
||||
if _, err := govalidator.ValidateStruct(acl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userGroups []*UserGroup
|
||||
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
|
||||
var userGroups []*dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("usergroup"))).Find(&userGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
acl.UserGroups = append(acl.UserGroups, userGroups...)
|
||||
var hostGroups []*HostGroup
|
||||
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
|
||||
var hostGroups []*dbmodels.HostGroup
|
||||
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("hostgroup"))).Find(&hostGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
acl.HostGroups = append(acl.HostGroups, hostGroups...)
|
||||
@@ -139,8 +144,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var acls []ACL
|
||||
if err := ACLsPreload(ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
|
||||
var acls []dbmodels.ACL
|
||||
if err := dbmodels.ACLsPreload(dbmodels.ACLsByIdentifiers(db, c.Args())).Find(&acls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,10 +165,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var acls []*ACL
|
||||
var acls []*dbmodels.ACL
|
||||
query := db.Order("created_at desc").Preload("UserGroups").Preload("HostGroups")
|
||||
if c.Bool("latest") {
|
||||
var acl ACL
|
||||
var acl dbmodels.ACL
|
||||
if err := query.First(&acl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -221,7 +226,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return ACLsByIdentifiers(db, c.Args()).Delete(&ACL{}).Error
|
||||
return dbmodels.ACLsByIdentifiers(db, c.Args()).Delete(&dbmodels.ACL{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
@@ -245,15 +250,15 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var acls []ACL
|
||||
if err := ACLsByIdentifiers(db, c.Args()).Find(&acls).Error; err != nil {
|
||||
var acls []dbmodels.ACL
|
||||
if err := dbmodels.ACLsByIdentifiers(db, c.Args()).Find(&acls).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx := db.Begin()
|
||||
for _, acl := range acls {
|
||||
model := tx.Model(&acl)
|
||||
update := ACL{
|
||||
update := dbmodels.ACL{
|
||||
Action: c.String("action"),
|
||||
HostPattern: c.String("pattern"),
|
||||
Weight: c.Uint("weight"),
|
||||
@@ -265,13 +270,13 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
// associations
|
||||
var appendUserGroups []UserGroup
|
||||
var deleteUserGroups []UserGroup
|
||||
if err := UserGroupsByIdentifiers(db, c.StringSlice("assign-usergroup")).Find(&appendUserGroups).Error; err != nil {
|
||||
var appendUserGroups []dbmodels.UserGroup
|
||||
var deleteUserGroups []dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-usergroup")).Find(&appendUserGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := UserGroupsByIdentifiers(db, c.StringSlice("unassign-usergroup")).Find(&deleteUserGroups).Error; err != nil {
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-usergroup")).Find(&deleteUserGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -286,13 +291,13 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
|
||||
var appendHostGroups []HostGroup
|
||||
var deleteHostGroups []HostGroup
|
||||
if err := HostGroupsByIdentifiers(db, c.StringSlice("assign-hostgroup")).Find(&appendHostGroups).Error; err != nil {
|
||||
var appendHostGroups []dbmodels.HostGroup
|
||||
var deleteHostGroups []dbmodels.HostGroup
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-hostgroup")).Find(&appendHostGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := HostGroupsByIdentifiers(db, c.StringSlice("unassign-hostgroup")).Find(&deleteHostGroups).Error; err != nil {
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-hostgroup")).Find(&deleteHostGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -330,62 +335,62 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
if err := HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
||||
config := dbmodels.Config{}
|
||||
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := SSHKeysPreload(db).Find(&config.SSHKeys).Error; err != nil {
|
||||
if err := dbmodels.SSHKeysPreload(db).Find(&config.SSHKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range config.SSHKeys {
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, key)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, key := range config.SSHKeys {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, key); err != nil {
|
||||
if err := crypto.SSHKeyEncrypt(actx.aesKey, key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
||||
if err := dbmodels.HostsPreload(db).Find(&config.Hosts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
}
|
||||
if !c.Bool("decrypt") {
|
||||
for _, host := range config.Hosts {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := UserKeysPreload(db).Find(&config.UserKeys).Error; err != nil {
|
||||
if err := dbmodels.UserKeysPreload(db).Find(&config.UserKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := UsersPreload(db).Find(&config.Users).Error; err != nil {
|
||||
if err := dbmodels.UsersPreload(db).Find(&config.Users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := UserGroupsPreload(db).Find(&config.UserGroups).Error; err != nil {
|
||||
if err := dbmodels.UserGroupsPreload(db).Find(&config.UserGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := HostGroupsPreload(db).Find(&config.HostGroups).Error; err != nil {
|
||||
if err := dbmodels.HostGroupsPreload(db).Find(&config.HostGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ACLsPreload(db).Find(&config.ACLs).Error; err != nil {
|
||||
if err := dbmodels.ACLsPreload(db).Find(&config.ACLs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Find(&config.Settings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
|
||||
if err := dbmodels.SessionsPreload(db).Find(&config.Sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !c.Bool("ignore-events") {
|
||||
if err := EventsPreload(db).Find(&config.Events).Error; err != nil {
|
||||
if err := dbmodels.EventsPreload(db).Find(&config.Events).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -409,7 +414,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
config := dbmodels.Config{}
|
||||
|
||||
dec := json.NewDecoder(s)
|
||||
if err := dec.Decode(&config); err != nil {
|
||||
@@ -472,9 +477,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, host := range config.Hosts {
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -508,9 +513,9 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
for _, sshKey := range config.SSHKeys {
|
||||
SSHKeyDecrypt(actx.config.aesKey, sshKey)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, sshKey)
|
||||
if !c.Bool("decrypt") {
|
||||
if err := SSHKeyEncrypt(actx.config.aesKey, sshKey); err != nil {
|
||||
if err := crypto.SSHKeyEncrypt(actx.aesKey, sshKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -570,8 +575,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var events []*Event
|
||||
if err := EventsPreload(EventsByIdentifiers(db, c.Args())).Find(&events).Error; err != nil {
|
||||
var events []*dbmodels.Event
|
||||
if err := dbmodels.EventsPreload(dbmodels.EventsByIdentifiers(db, c.Args())).Find(&events).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -599,10 +604,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var events []Event
|
||||
var events []dbmodels.Event
|
||||
query := db.Order("created_at desc").Preload("Author")
|
||||
if c.Bool("latest") {
|
||||
var event Event
|
||||
var event dbmodels.Event
|
||||
if err := query.First(&event).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -670,20 +675,29 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := ParseInputURL(c.Args().First())
|
||||
u, err := parseInputURL(c.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host := &Host{
|
||||
host := &dbmodels.Host{
|
||||
URL: u.String(),
|
||||
Comment: c.String("comment"),
|
||||
}
|
||||
if c.String("password") != "" {
|
||||
host.Password = c.String("password")
|
||||
}
|
||||
host.Name = strings.Split(host.Hostname(), ".")[0]
|
||||
matched, err := regexp.MatchString(`^([0-9]{1,3}.){3}.([0-9]{1,3})$`, host.Hostname())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matched {
|
||||
host.Name = host.Hostname()
|
||||
} else {
|
||||
host.Name = strings.Split(host.Hostname(), ".")[0]
|
||||
}
|
||||
|
||||
if c.String("hop") != "" {
|
||||
hop, err := HostByName(db, c.String("hop"))
|
||||
hop, err := dbmodels.HostByName(db, c.String("hop"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -703,8 +717,8 @@ GLOBAL OPTIONS:
|
||||
inputKey = "default"
|
||||
}
|
||||
if inputKey != "" {
|
||||
var key SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, []string{inputKey}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
host.SSHKeyID = key.ID
|
||||
@@ -715,12 +729,12 @@ GLOBAL OPTIONS:
|
||||
if len(inputGroups) == 0 {
|
||||
inputGroups = []string{"default"}
|
||||
}
|
||||
if err := HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, inputGroups).Find(&host.Groups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// encrypt
|
||||
if err := HostEncrypt(actx.config.aesKey, host); err != nil {
|
||||
if err := crypto.HostEncrypt(actx.aesKey, host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -746,18 +760,18 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hosts []*Host
|
||||
var hosts []*dbmodels.Host
|
||||
db = db.Preload("Groups")
|
||||
if myself.HasRole("admin") {
|
||||
db = db.Preload("SSHKey")
|
||||
}
|
||||
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
||||
if err := dbmodels.HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, host := range hosts {
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,10 +791,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hosts []*Host
|
||||
var hosts []*dbmodels.Host
|
||||
query := db.Order("created_at desc").Preload("Groups")
|
||||
if c.Bool("latest") {
|
||||
var host Host
|
||||
var host dbmodels.Host
|
||||
if err := query.First(&host).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -805,7 +819,7 @@ GLOBAL OPTIONS:
|
||||
for _, host := range hosts {
|
||||
authKey := ""
|
||||
if host.SSHKeyID > 0 {
|
||||
var key SSHKey
|
||||
var key dbmodels.SSHKey
|
||||
db.Model(&host).Related(&key)
|
||||
authKey = key.Name
|
||||
}
|
||||
@@ -815,7 +829,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
var hop string
|
||||
if host.HopID != 0 {
|
||||
var hopHost Host
|
||||
var hopHost dbmodels.Host
|
||||
db.Model(&host).Related(&hopHost, "HopID")
|
||||
hop = hopHost.Name
|
||||
} else {
|
||||
@@ -850,7 +864,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return HostsByIdentifiers(db, c.Args()).Delete(&Host{}).Error
|
||||
return dbmodels.HostsByIdentifiers(db, c.Args()).Delete(&dbmodels.Host{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
@@ -875,8 +889,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hosts []Host
|
||||
if err := HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
||||
var hosts []dbmodels.Host
|
||||
if err := dbmodels.HostsByIdentifiers(db, c.Args()).Find(&hosts).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -899,7 +913,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
// url
|
||||
if c.String("url") != "" {
|
||||
u, err := ParseInputURL(c.String("url"))
|
||||
u, err := parseInputURL(c.String("url"))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -912,7 +926,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
// hop
|
||||
if c.String("hop") != "" {
|
||||
hop, err := HostByName(db, c.String("hop"))
|
||||
hop, err := dbmodels.HostByName(db, c.String("hop"))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -925,7 +939,7 @@ GLOBAL OPTIONS:
|
||||
|
||||
// remove the hop
|
||||
if c.Bool("unset-hop") {
|
||||
var hopHost Host
|
||||
var hopHost dbmodels.Host
|
||||
|
||||
db.Model(&host).Related(&hopHost, "HopID")
|
||||
if err := model.Association("Hop").Clear().Error; err != nil {
|
||||
@@ -936,8 +950,8 @@ GLOBAL OPTIONS:
|
||||
|
||||
// associations
|
||||
if c.String("key") != "" {
|
||||
var key SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, []string{c.String("key")}).First(&key).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -946,13 +960,13 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
}
|
||||
var appendGroups []HostGroup
|
||||
var deleteGroups []HostGroup
|
||||
if err := HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
||||
var appendGroups []dbmodels.HostGroup
|
||||
var deleteGroups []dbmodels.HostGroup
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -989,7 +1003,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
hostGroup := HostGroup{
|
||||
hostGroup := dbmodels.HostGroup{
|
||||
Name: c.String("name"),
|
||||
Comment: c.String("comment"),
|
||||
}
|
||||
@@ -1020,8 +1034,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hostGroups []HostGroup
|
||||
if err := HostGroupsPreload(HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
|
||||
var hostGroups []dbmodels.HostGroup
|
||||
if err := dbmodels.HostGroupsPreload(dbmodels.HostGroupsByIdentifiers(db, c.Args())).Find(&hostGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1041,10 +1055,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hostGroups []*HostGroup
|
||||
var hostGroups []*dbmodels.HostGroup
|
||||
query := db.Order("created_at desc").Preload("ACLs").Preload("Hosts")
|
||||
if c.Bool("latest") {
|
||||
var hostGroup HostGroup
|
||||
var hostGroup dbmodels.HostGroup
|
||||
if err := query.First(&hostGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1094,7 +1108,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return HostGroupsByIdentifiers(db, c.Args()).Delete(&HostGroup{}).Error
|
||||
return dbmodels.HostGroupsByIdentifiers(db, c.Args()).Delete(&dbmodels.HostGroup{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
@@ -1113,8 +1127,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var hostgroups []HostGroup
|
||||
if err := HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
|
||||
var hostgroups []dbmodels.HostGroup
|
||||
if err := dbmodels.HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1147,14 +1161,14 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(s, "debug mode (server): %v\n", actx.config.debug)
|
||||
fmt.Fprintf(s, "debug mode (server): %v\n", actx.debug)
|
||||
hostname, _ := os.Hostname()
|
||||
fmt.Fprintf(s, "Hostname: %s\n", hostname)
|
||||
fmt.Fprintf(s, "CPUs: %d\n", runtime.NumCPU())
|
||||
fmt.Fprintf(s, "Demo mode: %v\n", actx.config.demo)
|
||||
fmt.Fprintf(s, "DB Driver: %s\n", actx.config.dbDriver)
|
||||
fmt.Fprintf(s, "DB Conn: %s\n", actx.config.dbURL)
|
||||
fmt.Fprintf(s, "Bind Address: %s\n", actx.config.bindAddr)
|
||||
fmt.Fprintf(s, "Demo mode: %v\n", actx.demo)
|
||||
fmt.Fprintf(s, "DB Driver: %s\n", actx.dbDriver)
|
||||
fmt.Fprintf(s, "DB Conn: %s\n", actx.dbURL)
|
||||
fmt.Fprintf(s, "Bind Address: %s\n", actx.bindAddr)
|
||||
fmt.Fprintf(s, "System Time: %v\n", time.Now().Format(time.RFC3339Nano))
|
||||
fmt.Fprintf(s, "OS Type: %s\n", runtime.GOOS)
|
||||
fmt.Fprintf(s, "OS Architecture: %s\n", runtime.GOARCH)
|
||||
@@ -1164,10 +1178,10 @@ GLOBAL OPTIONS:
|
||||
|
||||
fmt.Fprintf(s, "User ID: %v\n", myself.ID)
|
||||
fmt.Fprintf(s, "User email: %s\n", myself.Email)
|
||||
fmt.Fprintf(s, "Version: %s\n", Version)
|
||||
fmt.Fprintf(s, "GIT SHA: %s\n", GitSha)
|
||||
fmt.Fprintf(s, "GIT Branch: %s\n", GitBranch)
|
||||
fmt.Fprintf(s, "GIT Tag: %s\n", GitTag)
|
||||
fmt.Fprintf(s, "Version: %s\n", version)
|
||||
fmt.Fprintf(s, "GIT SHA: %s\n", gitSha)
|
||||
fmt.Fprintf(s, "GIT Branch: %s\n", gitBranch)
|
||||
fmt.Fprintf(s, "GIT Tag: %s\n", gitTag)
|
||||
|
||||
// FIXME: add info about current server (network, cpu, ram, OS)
|
||||
// FIXME: add info about current user
|
||||
@@ -1199,9 +1213,9 @@ GLOBAL OPTIONS:
|
||||
name = c.String("name")
|
||||
}
|
||||
|
||||
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
|
||||
if actx.config.aesKey != "" {
|
||||
if err2 := SSHKeyEncrypt(actx.config.aesKey, key); err2 != nil {
|
||||
key, err := crypto.NewSSHKey(c.String("type"), c.Uint("length"))
|
||||
if actx.aesKey != "" {
|
||||
if err2 := crypto.SSHKeyEncrypt(actx.aesKey, key); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
@@ -1256,7 +1270,7 @@ GLOBAL OPTIONS:
|
||||
break
|
||||
}
|
||||
}
|
||||
key, err := ImportSSHKey(value)
|
||||
key, err := crypto.ImportSSHKey(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1293,14 +1307,14 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var keys []*SSHKey
|
||||
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).Find(&keys).Error; err != nil {
|
||||
var keys []*dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).Find(&keys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("decrypt") {
|
||||
for _, key := range keys {
|
||||
SSHKeyDecrypt(actx.config.aesKey, key)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1320,10 +1334,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var sshKeys []*SSHKey
|
||||
var sshKeys []*dbmodels.SSHKey
|
||||
query := db.Order("created_at desc").Preload("Hosts")
|
||||
if c.Bool("latest") {
|
||||
var sshKey SSHKey
|
||||
var sshKey dbmodels.SSHKey
|
||||
if err := query.First(&sshKey).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1374,7 +1388,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return SSHKeysByIdentifiers(db, c.Args()).Delete(&SSHKey{}).Error
|
||||
return dbmodels.SSHKeysByIdentifiers(db, c.Args()).Delete(&dbmodels.SSHKey{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "setup",
|
||||
@@ -1387,8 +1401,8 @@ GLOBAL OPTIONS:
|
||||
|
||||
// not checking roles, everyone with an account can see how to enroll new hosts
|
||||
|
||||
var key SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, c.Args()).First(&key).Error; err != nil {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, c.Args()).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(s, "umask 077; mkdir -p .ssh; echo %s sshportal >> .ssh/authorized_keys\n", key.PubKey)
|
||||
@@ -1405,11 +1419,11 @@ GLOBAL OPTIONS:
|
||||
|
||||
// not checking roles, everyone with an account can see how to enroll new hosts
|
||||
|
||||
var key SSHKey
|
||||
if err := SSHKeysByIdentifiers(SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(dbmodels.SSHKeysPreload(db), c.Args()).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
SSHKeyDecrypt(actx.config.aesKey, &key)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, &key)
|
||||
|
||||
type line struct {
|
||||
key string
|
||||
@@ -1487,8 +1501,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var users []User
|
||||
if err := UsersPreload(UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
|
||||
var users []dbmodels.User
|
||||
if err := dbmodels.UsersPreload(dbmodels.UsersByIdentifiers(db, c.Args())).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1523,7 +1537,7 @@ GLOBAL OPTIONS:
|
||||
name = c.String("name")
|
||||
}
|
||||
|
||||
user := User{
|
||||
user := dbmodels.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Comment: c.String("comment"),
|
||||
@@ -1539,7 +1553,7 @@ GLOBAL OPTIONS:
|
||||
if len(inputGroups) == 0 {
|
||||
inputGroups = []string{"default"}
|
||||
}
|
||||
if err := UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, inputGroups).Find(&user.Groups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1562,10 +1576,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var users []*User
|
||||
var users []*dbmodels.User
|
||||
query := db.Order("created_at desc").Preload("Groups").Preload("Roles").Preload("Keys")
|
||||
if c.Bool("latest") {
|
||||
var user User
|
||||
var user dbmodels.User
|
||||
if err := query.First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1623,7 +1637,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return UsersByIdentifiers(db, c.Args()).Delete(&User{}).Error
|
||||
return dbmodels.UsersByIdentifiers(db, c.Args()).Delete(&dbmodels.User{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
@@ -1647,8 +1661,8 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
// FIXME: check if unset-admin + user == myself
|
||||
var users []User
|
||||
if err := UsersByIdentifiers(db, c.Args()).Find(&users).Error; err != nil {
|
||||
var users []dbmodels.User
|
||||
if err := dbmodels.UsersByIdentifiers(db, c.Args()).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1674,13 +1688,13 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
|
||||
// associations
|
||||
var appendGroups []UserGroup
|
||||
if err := UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
||||
var appendGroups []dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("assign-group")).Find(&appendGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
var deleteGroups []UserGroup
|
||||
if err := UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
||||
var deleteGroups []dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, c.StringSlice("unassign-group")).Find(&deleteGroups).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -1694,13 +1708,13 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
}
|
||||
var appendRoles []UserRole
|
||||
if err := UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
|
||||
var appendRoles []dbmodels.UserRole
|
||||
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("assign-role")).Find(&appendRoles).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
var deleteRoles []UserRole
|
||||
if err := UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil {
|
||||
var deleteRoles []dbmodels.UserRole
|
||||
if err := dbmodels.UserRolesByIdentifiers(db, c.StringSlice("unassign-role")).Find(&deleteRoles).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
@@ -1736,7 +1750,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
userGroup := UserGroup{
|
||||
userGroup := dbmodels.UserGroup{
|
||||
Name: c.String("name"),
|
||||
Comment: c.String("comment"),
|
||||
}
|
||||
@@ -1750,7 +1764,7 @@ GLOBAL OPTIONS:
|
||||
// FIXME: check if name already exists
|
||||
// FIXME: add myself to the new group
|
||||
|
||||
userGroup.Users = []*User{myself}
|
||||
userGroup.Users = []*dbmodels.User{myself}
|
||||
|
||||
if err := db.Create(&userGroup).Error; err != nil {
|
||||
return err
|
||||
@@ -1771,8 +1785,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var userGroups []UserGroup
|
||||
if err := UserGroupsPreload(UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
|
||||
var userGroups []dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsPreload(dbmodels.UserGroupsByIdentifiers(db, c.Args())).Find(&userGroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1792,10 +1806,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var userGroups []*UserGroup
|
||||
var userGroups []*dbmodels.UserGroup
|
||||
query := db.Order("created_at desc").Preload("ACLs").Preload("Users")
|
||||
if c.Bool("latest") {
|
||||
var userGroup UserGroup
|
||||
var userGroup dbmodels.UserGroup
|
||||
if err := query.First(&userGroup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1844,7 +1858,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return UserGroupsByIdentifiers(db, c.Args()).Delete(&UserGroup{}).Error
|
||||
return dbmodels.UserGroupsByIdentifiers(db, c.Args()).Delete(&dbmodels.UserGroup{}).Error
|
||||
},
|
||||
}, {
|
||||
Name: "update",
|
||||
@@ -1863,8 +1877,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var usergroups []UserGroup
|
||||
if err := UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
|
||||
var usergroups []dbmodels.UserGroup
|
||||
if err := dbmodels.UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1910,8 +1924,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var user User
|
||||
if err := UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
|
||||
var user dbmodels.User
|
||||
if err := dbmodels.UsersByIdentifiers(db, c.Args()).First(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1924,10 +1938,11 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
userkey := UserKey{
|
||||
User: &user,
|
||||
Key: key.Marshal(),
|
||||
Comment: comment,
|
||||
userkey := dbmodels.UserKey{
|
||||
User: &user,
|
||||
Key: key.Marshal(),
|
||||
Comment: comment,
|
||||
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
|
||||
}
|
||||
if c.String("comment") != "" {
|
||||
userkey.Comment = c.String("comment")
|
||||
@@ -1957,8 +1972,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var userKeys []UserKey
|
||||
if err := UserKeysPreload(UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
|
||||
var userKeys []dbmodels.UserKey
|
||||
if err := dbmodels.UserKeysPreload(dbmodels.UserKeysByIdentifiers(db, c.Args())).Find(&userKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1978,10 +1993,10 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var userKeys []*UserKey
|
||||
var userKeys []*dbmodels.UserKey
|
||||
query := db.Order("created_at desc").Preload("User")
|
||||
if c.Bool("latest") {
|
||||
var userKey UserKey
|
||||
var userKey dbmodels.UserKey
|
||||
if err := query.First(&userKey).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2032,7 +2047,7 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
return UserKeysByIdentifiers(db, c.Args()).Delete(&UserKey{}).Error
|
||||
return dbmodels.UserKeysByIdentifiers(db, c.Args()).Delete(&dbmodels.UserKey{}).Error
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2053,8 +2068,8 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var sessions []Session
|
||||
if err := SessionsPreload(SessionsByIdentifiers(db, c.Args())).Find(&sessions).Error; err != nil {
|
||||
var sessions []dbmodels.Session
|
||||
if err := dbmodels.SessionsPreload(dbmodels.SessionsByIdentifiers(db, c.Args())).Find(&sessions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2075,9 +2090,9 @@ GLOBAL OPTIONS:
|
||||
return err
|
||||
}
|
||||
|
||||
var sessions []*Session
|
||||
var sessions []*dbmodels.Session
|
||||
|
||||
limit, offset, status := 60000, -1, []string{string(SessionStatusActive), string(SessionStatusClosed), string(SessionStatusUnknown)}
|
||||
limit, offset, status := 60000, -1, []string{string(dbmodels.SessionStatusActive), string(dbmodels.SessionStatusClosed), string(dbmodels.SessionStatusUnknown)}
|
||||
if c.Bool("active") {
|
||||
status = status[:1]
|
||||
}
|
||||
@@ -2085,7 +2100,7 @@ GLOBAL OPTIONS:
|
||||
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
|
||||
|
||||
if c.Bool("latest") {
|
||||
var session Session
|
||||
var session dbmodels.Session
|
||||
if err := query.First(&session).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2098,7 +2113,7 @@ GLOBAL OPTIONS:
|
||||
factor := 1
|
||||
for len(sessions) >= limit*factor {
|
||||
|
||||
var additionnalSessions []*Session
|
||||
var additionnalSessions []*dbmodels.Session
|
||||
|
||||
offset = limit * factor
|
||||
query := db.Order("created_at desc").Limit(limit).Offset(offset).Where("status in (?)", status).Preload("User").Preload("Host")
|
||||
@@ -2156,7 +2171,7 @@ GLOBAL OPTIONS:
|
||||
Name: "version",
|
||||
Usage: "Shows the SSHPortal version information",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Fprintf(s, "%s\n", Version)
|
||||
fmt.Fprintf(s, "%s\n", version)
|
||||
return nil
|
||||
},
|
||||
}, {
|
||||
@@ -2187,7 +2202,7 @@ GLOBAL OPTIONS:
|
||||
if len(words) == 0 {
|
||||
continue
|
||||
}
|
||||
NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
|
||||
dbmodels.NewEvent("shell", words[0]).SetAuthor(myself).SetArg("interactive", true).SetArg("args", words[1:]).Log(db)
|
||||
if err := app.Run(append([]string{"config"}, words...)); err != nil {
|
||||
if cliErr, ok := err.(*cli.ExitError); ok {
|
||||
if cliErr.ExitCode() != 0 {
|
||||
@@ -2200,7 +2215,7 @@ GLOBAL OPTIONS:
|
||||
}
|
||||
}
|
||||
} else { // oneshot mode
|
||||
NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
|
||||
dbmodels.NewEvent("shell", sshCommand[0]).SetAuthor(myself).SetArg("interactive", false).SetArg("args", sshCommand[1:]).Log(db)
|
||||
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
|
||||
if errMsg := err.Error(); errMsg != "" {
|
||||
fmt.Fprintf(s, "error: %s\n", errMsg)
|
||||
@@ -2214,3 +2229,21 @@ GLOBAL OPTIONS:
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapText(in string, length int) string {
|
||||
if len(in) <= length {
|
||||
return in
|
||||
}
|
||||
return in[0:length-3] + "..."
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
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/bastionsession"
|
||||
"moul.io/sshportal/pkg/crypto"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type sshportalContextKey string
|
||||
@@ -21,40 +21,44 @@ type sshportalContextKey string
|
||||
var authContextKey = sshportalContextKey("auth")
|
||||
|
||||
type authContext struct {
|
||||
message string
|
||||
err error
|
||||
user User
|
||||
inputUsername string
|
||||
db *gorm.DB
|
||||
userKey UserKey
|
||||
config *configServe
|
||||
authMethod string
|
||||
authSuccess bool
|
||||
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
|
||||
}
|
||||
|
||||
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 *Host) gossh.HostKeyCallback {
|
||||
func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
|
||||
if len(host.HostKey) == 0 {
|
||||
log.Println("Discovering host fingerprint...")
|
||||
@@ -68,7 +72,9 @@ func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback {
|
||||
}
|
||||
}
|
||||
|
||||
func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
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) {
|
||||
switch newChan.ChannelType() {
|
||||
case "session":
|
||||
case "direct-tcpip":
|
||||
@@ -83,9 +89,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:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
|
||||
host, err := HostByName(actx.db, actx.inputUsername)
|
||||
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)
|
||||
if err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
if err2 != nil {
|
||||
@@ -98,8 +104,8 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
}
|
||||
|
||||
switch host.Scheme() {
|
||||
case BastionSchemeSSH:
|
||||
sessionConfigs := make([]bastionsession.Config, 0)
|
||||
case dbmodels.BastionSchemeSSH:
|
||||
sessionConfigs := make([]sessionConfig, 0)
|
||||
currentHost := host
|
||||
for currentHost != nil {
|
||||
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
|
||||
@@ -113,25 +119,25 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
sessionConfigs = append([]bastionsession.Config{{
|
||||
sessionConfigs = append([]sessionConfig{{
|
||||
Addr: currentHost.DialAddr(),
|
||||
ClientConfig: clientConfig,
|
||||
Logs: actx.config.logsLocation,
|
||||
Logs: actx.logsLocation,
|
||||
}}, sessionConfigs...)
|
||||
if currentHost.HopID != 0 {
|
||||
var newHost Host
|
||||
var newHost dbmodels.Host
|
||||
actx.db.Model(currentHost).Related(&newHost, "HopID")
|
||||
hostname := newHost.Name
|
||||
currentHost, _ = HostByName(actx.db, hostname)
|
||||
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
|
||||
} else {
|
||||
currentHost = nil
|
||||
}
|
||||
}
|
||||
|
||||
sess := Session{
|
||||
sess := dbmodels.Session{
|
||||
UserID: actx.user.ID,
|
||||
HostID: host.ID,
|
||||
Status: string(SessionStatusActive),
|
||||
Status: string(dbmodels.SessionStatusActive),
|
||||
}
|
||||
if err = actx.db.Create(&sess).Error; err != nil {
|
||||
ch, _, err2 := newChan.Accept()
|
||||
@@ -142,16 +148,15 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
_ = ch.Close()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs)
|
||||
err = multiChannelHandler(srv, conn, newChan, ctx, sessionConfigs, sess.ID)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sessUpdate := Session{
|
||||
Status: string(SessionStatusClosed),
|
||||
sessUpdate := dbmodels.Session{
|
||||
Status: string(dbmodels.SessionStatusClosed),
|
||||
ErrMsg: fmt.Sprintf("%v", err),
|
||||
StoppedAt: &now,
|
||||
}
|
||||
@@ -161,12 +166,12 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh
|
||||
}
|
||||
actx.db.Model(&sess).Updates(&sessUpdate)
|
||||
}()
|
||||
case BastionSchemeTelnet:
|
||||
case dbmodels.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 {
|
||||
@@ -177,37 +182,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 *Host) (*gossh.ClientConfig, error) {
|
||||
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
|
||||
actx := ctx.Value(authContextKey).(*authContext)
|
||||
|
||||
clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host))
|
||||
crypto.HostDecrypt(actx.aesKey, host)
|
||||
crypto.SSHKeyDecrypt(actx.aesKey, host.SSHKey)
|
||||
|
||||
clientConfig, err := host.ClientConfig(dynamicHostKey(actx.db, host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tmpUser User
|
||||
var tmpUser dbmodels.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 Host
|
||||
var tmpHost dbmodels.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)
|
||||
action, err2 := checkACLs(tmpUser, tmpHost)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
|
||||
HostDecrypt(actx.config.aesKey, host)
|
||||
SSHKeyDecrypt(actx.config.aesKey, host.SSHKey)
|
||||
|
||||
switch action {
|
||||
case string(ACLActionAllow):
|
||||
case string(ACLActionDeny):
|
||||
case string(dbmodels.ACLActionAllow):
|
||||
case string(dbmodels.ACLActionDeny):
|
||||
return nil, fmt.Errorf("you don't have permission to that host")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL action: %q", action)
|
||||
@@ -215,10 +220,10 @@ func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, erro
|
||||
return clientConfig, nil
|
||||
}
|
||||
|
||||
func shellHandler(s ssh.Session) {
|
||||
func ShellHandler(s ssh.Session, version, gitSha, gitTag, gitBranch string) {
|
||||
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:%q,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:%d,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
|
||||
}
|
||||
|
||||
if actx.err != nil {
|
||||
@@ -232,43 +237,48 @@ func shellHandler(s ssh.Session) {
|
||||
}
|
||||
|
||||
switch actx.userType() {
|
||||
case UserTypeHealthcheck:
|
||||
case userTypeHealthcheck:
|
||||
fmt.Fprintln(s, "OK")
|
||||
return
|
||||
case UserTypeShell:
|
||||
if err := shell(s); err != nil {
|
||||
case userTypeShell:
|
||||
if err := shell(s, version, gitSha, gitTag, gitBranch); 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, cfg *configServe) ssh.PasswordHandler {
|
||||
func PasswordAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
|
||||
return func(ctx ssh.Context, pass string) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
config: cfg,
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
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 SSHKey
|
||||
if err := SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
var key dbmodels.SSHKey
|
||||
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
SSHKeyDecrypt(aesKey, &key)
|
||||
crypto.SSHKeyDecrypt(aesKey, &key)
|
||||
|
||||
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
|
||||
if err != nil {
|
||||
@@ -279,12 +289,17 @@ func privateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
|
||||
}
|
||||
}
|
||||
|
||||
func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
|
||||
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
|
||||
return func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
actx := &authContext{
|
||||
db: db,
|
||||
inputUsername: ctx.User(),
|
||||
config: cfg,
|
||||
logsLocation: logsLocation,
|
||||
aesKey: aesKey,
|
||||
dbDriver: dbDriver,
|
||||
dbURL: dbURL,
|
||||
bindAddr: bindAddr,
|
||||
demo: demo,
|
||||
authMethod: "pubkey",
|
||||
authSuccess: true,
|
||||
}
|
||||
@@ -294,20 +309,20 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
|
||||
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 = UserKey{
|
||||
actx.userKey = dbmodels.UserKey{
|
||||
UserID: actx.user.ID,
|
||||
Key: key.Marshal(),
|
||||
Comment: "created by sshportal",
|
||||
@@ -321,7 +336,7 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
|
||||
|
||||
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 = User{Name: "Anonymous"}
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
actx.err = errors.New("your token is invalid or expired")
|
||||
}
|
||||
return true
|
||||
@@ -329,7 +344,7 @@ func publicKeyAuthHandler(db *gorm.DB, cfg *configServe) ssh.PublicKeyHandler {
|
||||
|
||||
// fallback
|
||||
actx.err = errors.New("unknown ssh key")
|
||||
actx.user = User{Name: "Anonymous"}
|
||||
actx.user = dbmodels.User{Name: "Anonymous"}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package bastion // import "moul.io/sshportal/pkg/bastion"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/moul/ssh"
|
||||
"github.com/gliderlabs/ssh"
|
||||
oi "github.com/reiver/go-oi"
|
||||
telnet "github.com/reiver/go-telnet"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
type bastionTelnetCaller struct {
|
||||
@@ -75,7 +76,7 @@ func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err e
|
||||
return bufio.ScanLines(data, atEOF)
|
||||
}
|
||||
|
||||
func telnetHandler(host *Host) ssh.Handler {
|
||||
func telnetHandler(host *dbmodels.Host) ssh.Handler {
|
||||
return func(s ssh.Session) {
|
||||
// FIXME: log session in db
|
||||
//actx := s.Context().Value(authContextKey).(*authContext)
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package crypto // import "moul.io/sshportal/pkg/crypto"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
"strings"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"moul.io/sshportal/pkg/dbmodels"
|
||||
)
|
||||
|
||||
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
|
||||
key := SSHKey{
|
||||
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: keyType,
|
||||
Length: length,
|
||||
}
|
||||
@@ -53,8 +54,8 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ImportSSHKey(keyValue string) (*SSHKey, error) {
|
||||
key := SSHKey{
|
||||
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
|
||||
key := dbmodels.SSHKey{
|
||||
Type: "rsa",
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ func safeDecrypt(key []byte, cryptoText string) string {
|
||||
return out
|
||||
}
|
||||
|
||||
func HostEncrypt(aesKey string, host *Host) (err error) {
|
||||
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -141,7 +142,7 @@ func HostEncrypt(aesKey string, host *Host) (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
func HostDecrypt(aesKey string, host *Host) {
|
||||
func HostDecrypt(aesKey string, host *dbmodels.Host) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
@@ -150,14 +151,14 @@ func HostDecrypt(aesKey string, host *Host) {
|
||||
}
|
||||
}
|
||||
|
||||
func SSHKeyEncrypt(aesKey string, key *SSHKey) (err error) {
|
||||
func SSHKeyEncrypt(aesKey string, key *dbmodels.SSHKey) (err error) {
|
||||
if aesKey == "" {
|
||||
return nil
|
||||
}
|
||||
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
|
||||
return
|
||||
}
|
||||
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
|
||||
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
|
||||
if aesKey == "" {
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -53,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),unix_user"`
|
||||
Name string `gorm:"size:32" valid:"required,length(1|32)"`
|
||||
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
|
||||
@@ -70,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:"required,length(1|10000)"`
|
||||
Key []byte `sql:"size:10000" valid:"length(1|10000)"`
|
||||
AuthorizedKey string `sql:"size:10000" valid:"required,length(1|10000)"`
|
||||
UserID uint ``
|
||||
User *User `gorm:"ForeignKey:UserID"`
|
||||
@@ -180,16 +180,6 @@ func init() {
|
||||
|
||||
// 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())
|
||||
}
|
||||
@@ -301,7 +291,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,
|
||||
134
server.go
Normal file
134
server.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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, Version, GitSha, GitTag, GitBranch) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
|
||||
Version: fmt.Sprintf("sshportal-%s", Version),
|
||||
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)
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
"github.com/moul/ssh"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
20
util.go
20
util.go
@@ -1,20 +0,0 @@
|
||||
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] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user