mirror of https://github.com/coder/code-server.git
parent
ff1da17496
commit
1dd7e4b4e1
|
@ -297,6 +297,9 @@ and then restart `code-server` with:
|
||||||
sudo systemctl restart code-server@$USER
|
sudo systemctl restart code-server@$USER
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Alternatively, you can specify the SHA-256 of your password at the `hashedPassword` field in the config file.
|
||||||
|
The `hashedPassword` field takes precedence over `password`.
|
||||||
|
|
||||||
### How do I securely access development web services?
|
### How do I securely access development web services?
|
||||||
|
|
||||||
If you're working on a web service and want to access it locally, `code-server` can proxy it for you.
|
If you're working on a web service and want to access it locally, `code-server` can proxy it for you.
|
||||||
|
|
|
@ -29,6 +29,7 @@ export interface Args extends VsArgs {
|
||||||
config?: string
|
config?: string
|
||||||
auth?: AuthType
|
auth?: AuthType
|
||||||
password?: string
|
password?: string
|
||||||
|
hashedPassword?: string
|
||||||
cert?: OptionalString
|
cert?: OptionalString
|
||||||
"cert-host"?: string
|
"cert-host"?: string
|
||||||
"cert-key"?: string
|
"cert-key"?: string
|
||||||
|
@ -104,6 +105,12 @@ const options: Options<Required<Args>> = {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
|
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
|
||||||
},
|
},
|
||||||
|
hashedPassword: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"The password hashed with SHA-256 for password authentication (can only be passed in via $HASHED_PASSWORD or the config file). \n" +
|
||||||
|
"Takes precedence over 'password'.",
|
||||||
|
},
|
||||||
cert: {
|
cert: {
|
||||||
type: OptionalString,
|
type: OptionalString,
|
||||||
path: true,
|
path: true,
|
||||||
|
@ -279,6 +286,10 @@ export const parse = (
|
||||||
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
|
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "hashedPassword" && !opts?.configFile) {
|
||||||
|
throw new Error("--hashedPassword can only be set in the config file or passed in via $HASHED_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
const option = options[key]
|
const option = options[key]
|
||||||
if (option.type === "boolean") {
|
if (option.type === "boolean") {
|
||||||
;(args[key] as boolean) = true
|
;(args[key] as boolean) = true
|
||||||
|
@ -361,6 +372,7 @@ export interface DefaultedArgs extends ConfigArgs {
|
||||||
"proxy-domain": string[]
|
"proxy-domain": string[]
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
usingEnvPassword: boolean
|
usingEnvPassword: boolean
|
||||||
|
usingEnvHashedPassword: boolean
|
||||||
"extensions-dir": string
|
"extensions-dir": string
|
||||||
"user-data-dir": string
|
"user-data-dir": string
|
||||||
}
|
}
|
||||||
|
@ -448,13 +460,20 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
|
||||||
args["cert-key"] = certKey
|
args["cert-key"] = certKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const usingEnvPassword = !!process.env.PASSWORD
|
let usingEnvPassword = !!process.env.PASSWORD
|
||||||
if (process.env.PASSWORD) {
|
if (process.env.PASSWORD) {
|
||||||
args.password = process.env.PASSWORD
|
args.password = process.env.PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it's not readable by child processes.
|
const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
|
||||||
|
if (process.env.HASHED_PASSWORD) {
|
||||||
|
args.hashedPassword = process.env.HASHED_PASSWORD
|
||||||
|
usingEnvPassword = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure they're not readable by child processes.
|
||||||
delete process.env.PASSWORD
|
delete process.env.PASSWORD
|
||||||
|
delete process.env.HASHED_PASSWORD
|
||||||
|
|
||||||
// Filter duplicate proxy domains and remove any leading `*.`.
|
// Filter duplicate proxy domains and remove any leading `*.`.
|
||||||
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
|
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
|
||||||
|
@ -463,6 +482,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
|
||||||
return {
|
return {
|
||||||
...args,
|
...args,
|
||||||
usingEnvPassword,
|
usingEnvPassword,
|
||||||
|
usingEnvHashedPassword,
|
||||||
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,8 +99,10 @@ const main = async (args: DefaultedArgs): Promise<void> => {
|
||||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||||
|
|
||||||
if (args.auth === AuthType.Password && !args.password) {
|
if (args.auth === AuthType.Password && !args.password && !args.hashedPassword) {
|
||||||
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
throw new Error(
|
||||||
|
"Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [app, wsApp, server] = await createApp(args)
|
const [app, wsApp, server] = await createApp(args)
|
||||||
|
@ -114,6 +116,8 @@ const main = async (args: DefaultedArgs): Promise<void> => {
|
||||||
logger.info(" - Authentication is enabled")
|
logger.info(" - Authentication is enabled")
|
||||||
if (args.usingEnvPassword) {
|
if (args.usingEnvPassword) {
|
||||||
logger.info(" - Using password from $PASSWORD")
|
logger.info(" - Using password from $PASSWORD")
|
||||||
|
} else if (args.usingEnvHashedPassword) {
|
||||||
|
logger.info(" - Using password from $HASHED_PASSWORD")
|
||||||
} else {
|
} else {
|
||||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,12 @@ export const authenticated = (req: express.Request): boolean => {
|
||||||
return true
|
return true
|
||||||
case AuthType.Password:
|
case AuthType.Password:
|
||||||
// The password is stored in the cookie after being hashed.
|
// The password is stored in the cookie after being hashed.
|
||||||
return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password))
|
return !!(
|
||||||
|
req.cookies.key &&
|
||||||
|
(req.args.hashedPassword
|
||||||
|
? safeCompare(req.cookies.key, req.args.hashedPassword)
|
||||||
|
: req.args.password && safeCompare(req.cookies.key, hash(req.args.password)))
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported auth type ${req.args.auth}`)
|
throw new Error(`Unsupported auth type ${req.args.auth}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||||
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
|
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
|
||||||
if (req.args.usingEnvPassword) {
|
if (req.args.usingEnvPassword) {
|
||||||
passwordMsg = "Password was set from $PASSWORD."
|
passwordMsg = "Password was set from $PASSWORD."
|
||||||
|
} else if (req.args.usingEnvHashedPassword) {
|
||||||
|
passwordMsg = "Password was set from $HASHED_PASSWORD."
|
||||||
}
|
}
|
||||||
return replaceTemplates(
|
return replaceTemplates(
|
||||||
req,
|
req,
|
||||||
|
@ -65,7 +67,11 @@ router.post("/", async (req, res) => {
|
||||||
throw new Error("Missing password")
|
throw new Error("Missing password")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.args.password && safeCompare(req.body.password, req.args.password)) {
|
if (
|
||||||
|
req.args.hashedPassword
|
||||||
|
? safeCompare(hash(req.body.password), req.args.hashedPassword)
|
||||||
|
: req.args.password && safeCompare(req.body.password, req.args.password)
|
||||||
|
) {
|
||||||
// The hash does not add any actual security but we do it for
|
// The hash does not add any actual security but we do it for
|
||||||
// obfuscation purposes (and as a side effect it handles escaping).
|
// obfuscation purposes (and as a side effect it handles escaping).
|
||||||
res.cookie(Cookie.Key, hash(req.body.password), {
|
res.cookie(Cookie.Key, hash(req.body.password), {
|
||||||
|
|
|
@ -26,6 +26,7 @@ describe("parser", () => {
|
||||||
port: 8080,
|
port: 8080,
|
||||||
"proxy-domain": [],
|
"proxy-domain": [],
|
||||||
usingEnvPassword: false,
|
usingEnvPassword: false,
|
||||||
|
usingEnvHashedPassword: false,
|
||||||
"extensions-dir": path.join(paths.data, "extensions"),
|
"extensions-dir": path.join(paths.data, "extensions"),
|
||||||
"user-data-dir": paths.data,
|
"user-data-dir": paths.data,
|
||||||
}
|
}
|
||||||
|
@ -290,6 +291,21 @@ describe("parser", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should use env var hashed password", async () => {
|
||||||
|
process.env.HASHED_PASSWORD = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" // test
|
||||||
|
const args = parse([])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(await setDefaults(args), {
|
||||||
|
...defaults,
|
||||||
|
_: [],
|
||||||
|
hashedPassword: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||||
|
usingEnvHashedPassword: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should filter proxy domains", async () => {
|
it("should filter proxy domains", async () => {
|
||||||
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
|
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
|
||||||
assert.deepEqual(args, {
|
assert.deepEqual(args, {
|
||||||
|
|
Loading…
Reference in New Issue