Repository: coder/code-server (TypeScript)
Date: 2022-02-28 14:07:07+00:00
Task type: Feature

Issue:
Add option to set unix socket permissions
Hello,

when using the --socket option, I can tell code-server which socket to use, but not the permissions. At the moment the default permissions are 0755, which means that only the user is able to write to the socket while it's world readable...

When running together with a web server, it'd be nice if it could be set to 0770 and giving the group name/id so that a common group between web server and code-server would be possible.

Something like:

--socket /var/run/code-server.sock,0770,user,group
--socket /var/run/code-server.sock,0770,,group

Also, the server doesn't clean up the socket when it goes down and on a restart it errors out with address already in use...

I'm using workarounds at the moment, but it would be better if code-server could take care of it on its own.




Issue discussion:
I'd agree with this. Setting users/groups seems a bit odd to me though. Is there an example of software you know that has this syntax?
Usually a program/system has a configuration file where these settings are defined in. As most of the socket related stuff is handled by systemd on a newer Linux system, the settings look something like this:

ListenStream=/run/snapd-snap.socket
SocketMode=0666
SocketUser=root
SocketGroup=root

You can also go with --socket-user --socket-group --socket-permissions if you prefer. This was just an idea I had, to keep it compact. 

Cu


Can you put the socket in a directory with whatever perms you need?
What do you mean by that?

Like creating a socket and then point code-server to it?

It's still a listening socket, even if it's a Unix socket. So the server has to create it with everything that belongs to it.

Cu

> Like creating a socket and then point code-server to it?

Create the directory for the socket and put whatever permissions you want on that directory. Then when starting code-server make the path for the socket be inside that directory.

See https://stackoverflow.com/a/21568011/4283659
> I'd agree with this. Setting users/groups seems a bit odd to me though. Is there an example of software you know that has this syntax?

php-fpm allows you to set socket's user, group, and permissions. Systemd itself (which runs pretty much every Linux service on a running host) allows you to set socket user, group, and permissions.
> php-fpm allows you to set socket's user, group, and permissions. Systemd itself (which runs pretty much every Linux service on a running host) allows you to set socket user, group, and permissions.

To clarify, @kylecarbs is asking for examples regarding just the syntax, not whether socket permissions can be set in other software.
Going to close as I believe a directory with permission restrictions is enough. If not, please comment and I'll reopen.
It's a common thing. A UNIX socket is represented by a file on the file system and the only way to protect it is to change the owner, group and the mode. Not offering this option is a security nightmare.

No. A directory around it to protect it is not an option.

> No. A directory around it to protect it is not an option.

Can you elaborate why not? I'm not hard set against it but given how easy it is to create a directory with whatever permissions you need, it's best we not add more options to code-server.

Either way I'll reopen and do a survey of what other modern servers do and we can go from there.
Well,

the (7) UNIX man page says:
```
Pathname socket ownership and permissions
       In the Linux implementation, pathname sockets honor the permissions of the directory they are in.  Creation of a new socket fails if the process does not have write and search (execute) permission on the directory in which the socket is created.

       On  Linux, connecting to a stream socket object requires write permission on that socket; sending a datagram to a datagram socket likewise requires write permission on that socket.  POSIX does not make any statement about the effect of the permissions on
       a socket file, and on some systems (e.g., older BSDs), the socket permissions are ignored.  Portable programs should not rely on this feature for security.
```
So this is a 50/50 thing. If this moves to a BSD before 4.2, then we could get into trouble, but other than that, it's just the way how a socket is made secure. I wonder if most people even know that some systems do not honor the file system permissions on UNIX sockets.

Cu




on Linux systems the file permissions are honored on the socket and as long as the connecting part is not able to 
> > Like creating a socket and then point code-server to it?
> 
> Create the directory for the socket and put whatever permissions you want on that directory. Then when starting code-server make the path for the socket be inside that directory.
> 
> See https://stackoverflow.com/a/21568011/4283659

I'm trying to run multiple instances of code-server on one development server. Instead of using ports, it seems cleaner to give each developer their own socket. I tried to follow your instructions and created /var/run/code-server owned by user/group www-data:www-data. I add the user that code-server runs under to the www-data group, however when I run code-server, I get a permission denied error. My goal is to use nginx to proxy each user's subdomain to the unix socket connected to the code-server for their home folder. Any insight you can provide would be really appreciated. Thank you!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no activity occurs in the next 5 days.

This feature seems necessary in my case.

I run code-server as user 1000, so I can get same experience as my code-oss. However, when trying to rev proxy code-server using NGINX, which is running as user http, I got permission errors.

As the socket file is owned by user 1000 and has 755 permission, any other user have no chance to connect it because they lack the write permission.

It's hard to workaround since the socket is recreated every time code-server starts.

Sorry for any disturbance.
> > No. A directory around it to protect it is not an option.
> 
> Can you elaborate why not? I'm not hard set against it but given how easy it is to create a directory with whatever permissions you need, it's best we not add more options to code-server.
> 
> Either way I'll reopen and do a survey of what other modern servers do and we can go from there.

In case you're using reverse proxy web server (e.g. NGINX) you need to ensure that NGINX can **write** to this socket.

Most web server bundled with distros are running with `www-data`, `apache`, `nobody`, ... user.

The socket created by code-server has default permission 0755 (owner has write permission) with the user:group of the **owner** (who run it).

This mean most web server can not write to the code-server socket and the proxy would never work.

---

In my use case, I just need some option to set the socket permission to 0777 so that my NGINX can write to this socket and the proxy just works.

Solution patch:
diff --git a/src/node/app.ts b/src/node/app.ts
--- a/src/node/app.ts
+++ b/src/node/app.ts
@@ -11,7 +11,7 @@ import { disposer } from "./http"
 import { isNodeJSErrnoException } from "./util"
 import { handleUpgrade } from "./wsRouter"
 
-type ListenOptions = Pick<DefaultedArgs, "socket" | "port" | "host">
+type ListenOptions = Pick<DefaultedArgs, "socket-mode" | "socket" | "port" | "host">
 
 export interface App extends Disposable {
   /** Handles regular HTTP requests. */
@@ -22,7 +22,7 @@ export interface App extends Disposable {
   server: http.Server
 }
 
-const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
+const listen = (server: http.Server, { host, port, socket, "socket-mode": mode }: ListenOptions) => {
   return new Promise<void>(async (resolve, reject) => {
     server.on("error", reject)
 
@@ -31,7 +31,16 @@ const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
       server.off("error", reject)
       server.on("error", (err) => util.logError(logger, "http server error", err))
 
-      resolve()
+      if (socket && mode) {
+        fs.chmod(socket, mode)
+          .then(resolve)
+          .catch((err) => {
+            util.logError(logger, "socket chmod", err)
+            reject(err)
+          })
+      } else {
+        resolve()
+      }
     }
 
     if (socket) {
diff --git a/src/node/cli.ts b/src/node/cli.ts
--- a/src/node/cli.ts
+++ b/src/node/cli.ts
@@ -56,6 +56,7 @@ export interface UserProvidedArgs {
   open?: boolean
   "bind-addr"?: string
   socket?: string
+  "socket-mode"?: string
   version?: boolean
   "proxy-domain"?: string[]
   "reuse-window"?: boolean
@@ -175,6 +176,7 @@ const options: Options<Required<UserProvidedArgs>> = {
   port: { type: "number", description: "" },
 
   socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
+  "socket-mode": { type: "string", description: "File mode of the socket." },
   version: { type: "boolean", short: "v", description: "Display version information." },
   _: { type: "string[]" },
 
@@ -513,6 +515,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
     args.host = "localhost"
     args.port = 0
     args.socket = undefined
+    args["socket-mode"] = undefined
     args.cert = undefined
     args.auth = AuthType.None
   }


Test patch:
diff --git a/test/unit/node/app.test.ts b/test/unit/node/app.test.ts
--- a/test/unit/node/app.test.ts
+++ b/test/unit/node/app.test.ts
@@ -107,6 +107,18 @@ describe("createApp", () => {
     app.dispose()
   })
 
+  it("should change the file mode of a socket", async () => {
+    const defaultArgs = await setDefaults({
+      socket: tmpFilePath,
+      "socket-mode": "777",
+    })
+
+    const app = await createApp(defaultArgs)
+
+    expect((await promises.stat(tmpFilePath)).mode & 0o777).toBe(0o777)
+    app.dispose()
+  })
+
   it("should create an https server if args.cert exists", async () => {
     const testCertificate = await generateCertificate("localhost")
     const cert = new OptionalString(testCertificate.cert)
diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts
--- a/test/unit/node/cli.test.ts
+++ b/test/unit/node/cli.test.ts
@@ -73,6 +73,8 @@ describe("parser", () => {
 
           "--socket=mumble",
 
+          "--socket-mode=777",
+
           "3",
 
           ["--user-data-dir", "path/to/user/dir"],
@@ -110,6 +112,7 @@ describe("parser", () => {
       open: true,
       port: 8081,
       socket: path.resolve("mumble"),
+      "socket-mode": "777",
       verbose: true,
       version: true,
       "bind-addr": "192.169.0.1:8080",
@@ -269,7 +272,9 @@ describe("parser", () => {
   })
 
   it("should override with --link", async () => {
-    const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" "))
+    const args = parse(
+      "--cert test --cert-key test --socket test --socket-mode 777 --host 0.0.0.0 --port 8888 --link test".split(" "),
+    )
     const defaultArgs = await setDefaults(args)
     expect(defaultArgs).toEqual({
       ...defaults,
@@ -282,6 +287,7 @@ describe("parser", () => {
       cert: undefined,
       "cert-key": path.resolve("test"),
       socket: undefined,
+      "socket-mode": undefined,
     })
   })